@5stones/react-native-audio-browser 0.1.4 → 0.1.6

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 (150) hide show
  1. package/android/src/main/java/com/audiobrowser/player/MediaFactory.kt +11 -71
  2. package/android/src/main/java/com/audiobrowser/player/TransformingDataSource.kt +125 -0
  3. package/ios/Browser/BrowserManager.swift +17 -2
  4. package/ios/TrackPlayer.swift +145 -31
  5. package/lib/commonjs/features/browser.js.map +1 -1
  6. package/lib/commonjs/features/equalizer.js.map +1 -1
  7. package/lib/commonjs/features/errors.js.map +1 -1
  8. package/lib/commonjs/features/favorites.js.map +1 -1
  9. package/lib/commonjs/features/metadata.js.map +1 -1
  10. package/lib/commonjs/features/nowPlaying.js.map +1 -1
  11. package/lib/commonjs/features/output.js.map +1 -1
  12. package/lib/commonjs/features/playback/state.js.map +1 -1
  13. package/lib/commonjs/features/player/options.js.map +1 -1
  14. package/lib/commonjs/features/queue/activeTrack.js.map +1 -1
  15. package/lib/commonjs/features/queue/queue.js +1 -1
  16. package/lib/commonjs/features/queue/queue.js.map +1 -1
  17. package/lib/commonjs/features/remoteControls.js.map +1 -1
  18. package/lib/commonjs/utils/useDebug.js.map +1 -1
  19. package/lib/commonjs/utils/validation.js +23 -0
  20. package/lib/commonjs/utils/validation.js.map +1 -0
  21. package/lib/commonjs/web/NativeAudioBrowser.js +53 -15
  22. package/lib/commonjs/web/NativeAudioBrowser.js.map +1 -1
  23. package/lib/commonjs/web/SimpleRouter.js +5 -4
  24. package/lib/commonjs/web/SimpleRouter.js.map +1 -1
  25. package/lib/commonjs/web/TrackPlayer/Player.js +49 -24
  26. package/lib/commonjs/web/TrackPlayer/Player.js.map +1 -1
  27. package/lib/commonjs/web/TrackPlayer/PlaylistPlayer.js +54 -45
  28. package/lib/commonjs/web/TrackPlayer/PlaylistPlayer.js.map +1 -1
  29. package/lib/commonjs/web/TrackPlayer/State.js.map +1 -1
  30. package/lib/commonjs/web/browser/BrowserManager.js +10 -17
  31. package/lib/commonjs/web/browser/BrowserManager.js.map +1 -1
  32. package/lib/commonjs/web/browser/FavoriteManager.js.map +1 -1
  33. package/lib/commonjs/web/browser/NavigationErrorManager.js.map +1 -1
  34. package/lib/commonjs/web/browser/SearchManager.js.map +1 -1
  35. package/lib/commonjs/web/http/HttpClient.js +15 -2
  36. package/lib/commonjs/web/http/HttpClient.js.map +1 -1
  37. package/lib/commonjs/web/http/RequestConfigBuilder.js.map +1 -1
  38. package/lib/commonjs/web/player/NowPlayingManager.js +9 -0
  39. package/lib/commonjs/web/player/NowPlayingManager.js.map +1 -1
  40. package/lib/commonjs/web/player/OptionsManager.js +1 -1
  41. package/lib/commonjs/web/player/OptionsManager.js.map +1 -1
  42. package/lib/commonjs/web/util/BrowserPathHelper.js.map +1 -1
  43. package/lib/module/features/browser.js.map +1 -1
  44. package/lib/module/features/equalizer.js.map +1 -1
  45. package/lib/module/features/errors.js.map +1 -1
  46. package/lib/module/features/favorites.js.map +1 -1
  47. package/lib/module/features/metadata.js.map +1 -1
  48. package/lib/module/features/nowPlaying.js +1 -0
  49. package/lib/module/features/nowPlaying.js.map +1 -1
  50. package/lib/module/features/output.js.map +1 -1
  51. package/lib/module/features/playback/state.js.map +1 -1
  52. package/lib/module/features/player/options.js.map +1 -1
  53. package/lib/module/features/queue/activeTrack.js.map +1 -1
  54. package/lib/module/features/queue/queue.js +1 -1
  55. package/lib/module/features/queue/queue.js.map +1 -1
  56. package/lib/module/features/remoteControls.js.map +1 -1
  57. package/lib/module/utils/useDebug.js.map +1 -1
  58. package/lib/module/utils/validation.js +18 -0
  59. package/lib/module/utils/validation.js.map +1 -0
  60. package/lib/module/web/NativeAudioBrowser.js +53 -15
  61. package/lib/module/web/NativeAudioBrowser.js.map +1 -1
  62. package/lib/module/web/SimpleRouter.js +5 -4
  63. package/lib/module/web/SimpleRouter.js.map +1 -1
  64. package/lib/module/web/TrackPlayer/Player.js +49 -24
  65. package/lib/module/web/TrackPlayer/Player.js.map +1 -1
  66. package/lib/module/web/TrackPlayer/PlaylistPlayer.js +54 -45
  67. package/lib/module/web/TrackPlayer/PlaylistPlayer.js.map +1 -1
  68. package/lib/module/web/TrackPlayer/State.js.map +1 -1
  69. package/lib/module/web/browser/BrowserManager.js +10 -17
  70. package/lib/module/web/browser/BrowserManager.js.map +1 -1
  71. package/lib/module/web/browser/FavoriteManager.js.map +1 -1
  72. package/lib/module/web/browser/NavigationErrorManager.js.map +1 -1
  73. package/lib/module/web/browser/SearchManager.js.map +1 -1
  74. package/lib/module/web/http/HttpClient.js +15 -2
  75. package/lib/module/web/http/HttpClient.js.map +1 -1
  76. package/lib/module/web/http/RequestConfigBuilder.js.map +1 -1
  77. package/lib/module/web/player/NowPlayingManager.js +9 -0
  78. package/lib/module/web/player/NowPlayingManager.js.map +1 -1
  79. package/lib/module/web/player/OptionsManager.js +1 -1
  80. package/lib/module/web/player/OptionsManager.js.map +1 -1
  81. package/lib/module/web/util/BrowserPathHelper.js.map +1 -1
  82. package/lib/typescript/src/features/browser.d.ts.map +1 -1
  83. package/lib/typescript/src/features/equalizer.d.ts.map +1 -1
  84. package/lib/typescript/src/features/errors.d.ts.map +1 -1
  85. package/lib/typescript/src/features/favorites.d.ts.map +1 -1
  86. package/lib/typescript/src/features/metadata.d.ts +1 -1
  87. package/lib/typescript/src/features/metadata.d.ts.map +1 -1
  88. package/lib/typescript/src/features/nowPlaying.d.ts +1 -1
  89. package/lib/typescript/src/features/nowPlaying.d.ts.map +1 -1
  90. package/lib/typescript/src/features/output.d.ts.map +1 -1
  91. package/lib/typescript/src/features/playback/state.d.ts +1 -1
  92. package/lib/typescript/src/features/playback/state.d.ts.map +1 -1
  93. package/lib/typescript/src/features/player/options.d.ts +1 -1
  94. package/lib/typescript/src/features/player/options.d.ts.map +1 -1
  95. package/lib/typescript/src/features/queue/activeTrack.d.ts.map +1 -1
  96. package/lib/typescript/src/features/queue/queue.d.ts.map +1 -1
  97. package/lib/typescript/src/features/remoteControls.d.ts.map +1 -1
  98. package/lib/typescript/src/specs/audio-browser.nitro.d.ts.map +1 -1
  99. package/lib/typescript/src/utils/useDebug.d.ts +1 -1
  100. package/lib/typescript/src/utils/useDebug.d.ts.map +1 -1
  101. package/lib/typescript/src/utils/validation.d.ts +3 -0
  102. package/lib/typescript/src/utils/validation.d.ts.map +1 -0
  103. package/lib/typescript/src/web/NativeAudioBrowser.d.ts +6 -1
  104. package/lib/typescript/src/web/NativeAudioBrowser.d.ts.map +1 -1
  105. package/lib/typescript/src/web/SimpleRouter.d.ts.map +1 -1
  106. package/lib/typescript/src/web/TrackPlayer/Player.d.ts +7 -19
  107. package/lib/typescript/src/web/TrackPlayer/Player.d.ts.map +1 -1
  108. package/lib/typescript/src/web/TrackPlayer/PlaylistPlayer.d.ts +3 -3
  109. package/lib/typescript/src/web/TrackPlayer/PlaylistPlayer.d.ts.map +1 -1
  110. package/lib/typescript/src/web/browser/BrowserManager.d.ts.map +1 -1
  111. package/lib/typescript/src/web/browser/FavoriteManager.d.ts.map +1 -1
  112. package/lib/typescript/src/web/browser/NavigationErrorManager.d.ts.map +1 -1
  113. package/lib/typescript/src/web/browser/SearchManager.d.ts +1 -1
  114. package/lib/typescript/src/web/browser/SearchManager.d.ts.map +1 -1
  115. package/lib/typescript/src/web/http/HttpClient.d.ts.map +1 -1
  116. package/lib/typescript/src/web/http/RequestConfigBuilder.d.ts.map +1 -1
  117. package/lib/typescript/src/web/player/NowPlayingManager.d.ts.map +1 -1
  118. package/lib/typescript/src/web/player/OptionsManager.d.ts.map +1 -1
  119. package/lib/typescript/src/web/util/BrowserPathHelper.d.ts.map +1 -1
  120. package/package.json +1 -1
  121. package/src/features/browser.ts +1 -1
  122. package/src/features/equalizer.ts +1 -1
  123. package/src/features/errors.ts +1 -1
  124. package/src/features/favorites.ts +1 -1
  125. package/src/features/metadata.ts +1 -1
  126. package/src/features/nowPlaying.ts +1 -1
  127. package/src/features/output.ts +1 -1
  128. package/src/features/playback/state.ts +1 -1
  129. package/src/features/player/options.ts +2 -2
  130. package/src/features/queue/activeTrack.ts +1 -1
  131. package/src/features/queue/queue.ts +2 -2
  132. package/src/features/remoteControls.ts +2 -2
  133. package/src/specs/audio-browser.nitro.ts +0 -1
  134. package/src/utils/useDebug.ts +6 -6
  135. package/src/utils/validation.ts +27 -0
  136. package/src/web/NativeAudioBrowser.ts +137 -58
  137. package/src/web/SimpleRouter.ts +24 -9
  138. package/src/web/TrackPlayer/Player.ts +58 -30
  139. package/src/web/TrackPlayer/PlaylistPlayer.ts +72 -63
  140. package/src/web/TrackPlayer/RepeatMode.ts +1 -1
  141. package/src/web/TrackPlayer/State.ts +9 -9
  142. package/src/web/browser/BrowserManager.ts +124 -67
  143. package/src/web/browser/FavoriteManager.ts +5 -3
  144. package/src/web/browser/NavigationErrorManager.ts +15 -8
  145. package/src/web/browser/SearchManager.ts +17 -11
  146. package/src/web/http/HttpClient.ts +25 -7
  147. package/src/web/http/RequestConfigBuilder.ts +29 -13
  148. package/src/web/player/NowPlayingManager.ts +13 -7
  149. package/src/web/player/OptionsManager.ts +7 -6
  150. package/src/web/util/BrowserPathHelper.ts +3 -2
@@ -1,9 +1,3 @@
1
- import type { AudioBrowser as AudioBrowserSpec, IosOutput } from '../specs/audio-browser.nitro'
2
- import type {
3
- ResolvedTrack,
4
- Track,
5
- } from '../types'
6
- import type { NativeBrowserConfiguration } from '../types/browser-native'
7
1
  import type {
8
2
  PlaybackErrorEvent,
9
3
  PlaybackError,
@@ -40,23 +34,32 @@ import type {
40
34
  BatteryOptimizationStatus,
41
35
  BatteryOptimizationStatusChangedEvent,
42
36
  BatteryWarningPendingChangedEvent,
43
- PartialSetupPlayerOptions,
37
+ PartialSetupPlayerOptions
44
38
  } from '../features'
45
- import { PlaylistPlayer, SleepTimerManager } from './TrackPlayer'
46
- import { HttpClient } from './http/HttpClient'
39
+ import type {
40
+ AudioBrowser as AudioBrowserSpec,
41
+ IosOutput
42
+ } from '../specs/audio-browser.nitro'
43
+ import type { ResolvedTrack, Track } from '../types'
44
+ import type { NativeBrowserConfiguration } from '../types/browser-native'
47
45
  import { BrowserManager } from './browser/BrowserManager'
48
46
  import { FavoriteManager } from './browser/FavoriteManager'
49
47
  import { NavigationErrorManager } from './browser/NavigationErrorManager'
50
48
  import { SearchManager } from './browser/SearchManager'
51
- import { OptionsManager } from './player/OptionsManager'
52
- import { NowPlayingManager } from './player/NowPlayingManager'
49
+ import { HttpClient } from './http/HttpClient'
53
50
  import { RequestConfigBuilder } from './http/RequestConfigBuilder'
51
+ import { NowPlayingManager } from './player/NowPlayingManager'
52
+ import { OptionsManager } from './player/OptionsManager'
53
+ import { PlaylistPlayer, SleepTimerManager } from './TrackPlayer'
54
54
  import { BrowserPathHelper } from './util/BrowserPathHelper'
55
55
 
56
56
  /**
57
57
  * Web implementation of AudioBrowser (unified browser + player)
58
58
  */
59
- export class NativeAudioBrowser extends PlaylistPlayer implements AudioBrowserSpec {
59
+ export class NativeAudioBrowser
60
+ extends PlaylistPlayer
61
+ implements AudioBrowserSpec
62
+ {
60
63
  // HybridObject stuff
61
64
  readonly name = 'WebAudioBrowser'
62
65
  equals() {
@@ -83,8 +86,10 @@ export class NativeAudioBrowser extends PlaylistPlayer implements AudioBrowserSp
83
86
  private nowPlayingManager: NowPlayingManager
84
87
 
85
88
  // Player state
89
+ private currentLoadId = 0
86
90
  private progressUpdateEventInterval: NodeJS.Timeout | undefined
87
- private _online: boolean = typeof navigator !== 'undefined' ? navigator.onLine : true
91
+ private _online: boolean =
92
+ typeof navigator !== 'undefined' ? navigator.onLine : true
88
93
  private onlineHandler: (() => void) | undefined
89
94
  private offlineHandler: (() => void) | undefined
90
95
  private sleepTimer = new (class extends SleepTimerManager {
@@ -128,17 +133,24 @@ export class NativeAudioBrowser extends PlaylistPlayer implements AudioBrowserSp
128
133
  onContentChanged: (content: ResolvedTrack | undefined) => void = () => {}
129
134
  onTabsChanged: (tabs: Track[]) => void = () => {}
130
135
  onNavigationError: (data: NavigationErrorEvent) => void = () => {}
131
- onFormattedNavigationError: (formattedError: FormattedNavigationError | undefined) => void = () => {}
136
+ onFormattedNavigationError: (
137
+ formattedError: FormattedNavigationError | undefined
138
+ ) => void = () => {}
132
139
 
133
140
  // MARK: Player event callbacks
134
141
  onChapterMetadata: (chapters: ChapterMetadata[]) => void = () => {}
135
142
  onTrackMetadata: (metadata: TrackMetadata) => void = () => {}
136
143
  onTimedMetadata: (metadata: TimedMetadata) => void = () => {}
137
- onPlaybackActiveTrackChanged: (data: PlaybackActiveTrackChangedEvent) => void = () => {}
144
+ onPlaybackActiveTrackChanged: (
145
+ data: PlaybackActiveTrackChangedEvent
146
+ ) => void = () => {}
138
147
  onPlaybackError: (data: PlaybackErrorEvent) => void = () => {}
139
- onPlaybackPlayWhenReadyChanged: (data: PlaybackPlayWhenReadyChangedEvent) => void = () => {}
148
+ onPlaybackPlayWhenReadyChanged: (
149
+ data: PlaybackPlayWhenReadyChangedEvent
150
+ ) => void = () => {}
140
151
  onPlaybackPlayingState: (data: PlayingState) => void = () => {}
141
- onPlaybackProgressUpdated: (data: PlaybackProgressUpdatedEvent) => void = () => {}
152
+ onPlaybackProgressUpdated: (data: PlaybackProgressUpdatedEvent) => void =
153
+ () => {}
142
154
  onPlaybackQueueEnded: (data: PlaybackQueueEndedEvent) => void = () => {}
143
155
  onPlaybackQueueChanged: (queue: Track[]) => void = () => {}
144
156
  onPlaybackRepeatModeChanged: (data: RepeatModeChangedEvent) => void = () => {}
@@ -165,25 +177,36 @@ export class NativeAudioBrowser extends PlaylistPlayer implements AudioBrowserSp
165
177
  onNowPlayingChanged: (metadata: NowPlayingMetadata) => void = () => {}
166
178
  onOnlineChanged: (online: boolean) => void = () => {}
167
179
  onEqualizerChanged: (settings: EqualizerSettings) => void = () => {}
168
- onBatteryWarningPendingChanged: (event: BatteryWarningPendingChangedEvent) => void = () => {}
169
- onBatteryOptimizationStatusChanged: (event: BatteryOptimizationStatusChangedEvent) => void = () => {}
180
+ onBatteryWarningPendingChanged: (
181
+ event: BatteryWarningPendingChangedEvent
182
+ ) => void = () => {}
183
+ onBatteryOptimizationStatusChanged: (
184
+ event: BatteryOptimizationStatusChangedEvent
185
+ ) => void = () => {}
170
186
  onSystemVolumeChanged: (volume: number) => void = () => {}
171
187
  onIosOutputChanged: (output: IosOutput) => void = () => {}
172
188
 
173
189
  // MARK: Remote handlers
174
190
  handleRemoteBookmark: (() => void) | undefined = undefined
175
191
  handleRemoteDislike: (() => void) | undefined = undefined
176
- handleRemoteJumpBackward: ((event: RemoteJumpBackwardEvent) => void) | undefined = undefined
177
- handleRemoteJumpForward: ((event: RemoteJumpForwardEvent) => void) | undefined = undefined
192
+ handleRemoteJumpBackward:
193
+ | ((event: RemoteJumpBackwardEvent) => void)
194
+ | undefined = undefined
195
+ handleRemoteJumpForward:
196
+ | ((event: RemoteJumpForwardEvent) => void)
197
+ | undefined = undefined
178
198
  handleRemoteLike: (() => void) | undefined = undefined
179
199
  handleRemoteNext: (() => void) | undefined = undefined
180
200
  handleRemotePause: (() => void) | undefined = undefined
181
201
  handleRemotePlay: (() => void) | undefined = undefined
182
- handleRemotePlayId: ((event: RemotePlayIdEvent) => void) | undefined = undefined
183
- handleRemotePlaySearch: ((event: RemotePlaySearchEvent) => void) | undefined = undefined
202
+ handleRemotePlayId: ((event: RemotePlayIdEvent) => void) | undefined =
203
+ undefined
204
+ handleRemotePlaySearch: ((event: RemotePlaySearchEvent) => void) | undefined =
205
+ undefined
184
206
  handleRemotePrevious: (() => void) | undefined = undefined
185
207
  handleRemoteSeek: ((event: RemoteSeekEvent) => void) | undefined = undefined
186
- handleRemoteSetRating: ((event: RemoteSetRatingEvent) => void) | undefined = undefined
208
+ handleRemoteSetRating: ((event: RemoteSetRatingEvent) => void) | undefined =
209
+ undefined
187
210
  handleRemoteSkip: (() => void) | undefined = undefined
188
211
  handleRemoteStop: (() => void) | undefined = undefined
189
212
 
@@ -204,19 +227,21 @@ export class NativeAudioBrowser extends PlaylistPlayer implements AudioBrowserSp
204
227
  this.navigationErrorManager
205
228
  )
206
229
 
207
- this.searchManager = new SearchManager(
208
- this.browserManager,
209
- this.httpClient
210
- )
230
+ this.searchManager = new SearchManager(this.browserManager, this.httpClient)
211
231
 
212
232
  // Wire up event callbacks from managers to class callbacks
213
233
  this.browserManager.onPathChanged = (path) => this.onPathChanged(path)
214
- this.browserManager.onContentChanged = (content) => this.onContentChanged(content)
234
+ this.browserManager.onContentChanged = (content) =>
235
+ this.onContentChanged(content)
215
236
  this.browserManager.onTabsChanged = (tabs) => this.onTabsChanged(tabs)
216
- this.navigationErrorManager.onNavigationError = (data) => this.onNavigationError(data)
217
- this.navigationErrorManager.onFormattedNavigationError = (error) => this.onFormattedNavigationError(error)
218
- this.optionsManager.onOptionsChanged = (options) => this.onOptionsChanged(options)
219
- this.nowPlayingManager.onNowPlayingChanged = (metadata) => this.onNowPlayingChanged(metadata)
237
+ this.navigationErrorManager.onNavigationError = (data) =>
238
+ this.onNavigationError(data)
239
+ this.navigationErrorManager.onFormattedNavigationError = (error) =>
240
+ this.onFormattedNavigationError(error)
241
+ this.optionsManager.onOptionsChanged = (options) =>
242
+ this.onOptionsChanged(options)
243
+ this.nowPlayingManager.onNowPlayingChanged = (metadata) =>
244
+ this.onNowPlayingChanged(metadata)
220
245
 
221
246
  // Setup online/offline listeners
222
247
  if (typeof window !== 'undefined') {
@@ -243,7 +268,8 @@ export class NativeAudioBrowser extends PlaylistPlayer implements AudioBrowserSp
243
268
  const didStateChange = newState.state !== oldState.state
244
269
  const didErrorChange =
245
270
  newState.state === 'error' && oldState.state === 'error'
246
- ? newState.error !== oldState.error
271
+ ? newState.error?.code !== oldState.error?.code ||
272
+ newState.error?.message !== oldState.error?.message
247
273
  : false
248
274
 
249
275
  super.state = newState
@@ -261,10 +287,18 @@ export class NativeAudioBrowser extends PlaylistPlayer implements AudioBrowserSp
261
287
  }
262
288
  }
263
289
 
290
+ /**
291
+ * Derives PlayingState from playback state and playWhenReady.
292
+ * Matches Android's PlayingStateFactory.derive() logic.
293
+ */
264
294
  private getPlayingStateFromPlayback(playback: Playback): PlayingState {
295
+ const state = playback.state
296
+ const pwr = this._playWhenReady
265
297
  return {
266
- playing: playback.state === 'playing',
267
- buffering: playback.state === 'buffering',
298
+ playing:
299
+ pwr && state !== 'error' && state !== 'ended' && state !== 'none',
300
+ buffering:
301
+ pwr && (state === 'loading' || state === 'buffering')
268
302
  }
269
303
  }
270
304
 
@@ -272,11 +306,17 @@ export class NativeAudioBrowser extends PlaylistPlayer implements AudioBrowserSp
272
306
  this.clearUpdateEventInterval()
273
307
  if (interval) {
274
308
  this.progressUpdateEventInterval = setInterval(() => {
275
- if (this.state.state === 'playing') {
309
+ const state = this.state.state
310
+ // Match Android: emit progress during loading, buffering, and playing
311
+ if (
312
+ state === 'playing' ||
313
+ state === 'loading' ||
314
+ state === 'buffering'
315
+ ) {
276
316
  const progress = this.getProgress()
277
317
  const event: PlaybackProgressUpdatedEvent = {
278
318
  ...progress,
279
- track: this.currentIndex || 0,
319
+ track: this.currentIndex || 0
280
320
  }
281
321
  this.onPlaybackProgressUpdated(event)
282
322
  }
@@ -294,7 +334,7 @@ export class NativeAudioBrowser extends PlaylistPlayer implements AudioBrowserSp
294
334
  super.onPlaylistEnded()
295
335
  this.onPlaybackQueueEnded({
296
336
  track: this.currentIndex ?? 0,
297
- position: this.element?.currentTime ?? 0,
337
+ position: this.element?.currentTime ?? 0
298
338
  })
299
339
  }
300
340
 
@@ -317,13 +357,16 @@ export class NativeAudioBrowser extends PlaylistPlayer implements AudioBrowserSp
317
357
  * @param parentPath The parent path to check against queueSourcePath
318
358
  * @returns true if successfully skipped to existing track, false otherwise
319
359
  */
320
- private trySkipToExistingQueueTrack(trackId: string, parentPath: string): boolean {
360
+ private trySkipToExistingQueueTrack(
361
+ trackId: string,
362
+ parentPath: string
363
+ ): boolean {
321
364
  if (parentPath !== this.browserManager.queueSourcePath) {
322
365
  return false
323
366
  }
324
367
 
325
368
  const queue = this.getQueue()
326
- const index = queue.findIndex(t => t.src === trackId)
369
+ const index = queue.findIndex((t) => t.src === trackId)
327
370
 
328
371
  if (index < 0) {
329
372
  return false
@@ -359,7 +402,10 @@ export class NativeAudioBrowser extends PlaylistPlayer implements AudioBrowserSp
359
402
  * Async implementation of track navigation with queue expansion support.
360
403
  * Matches Android's MediaSessionCallback behavior.
361
404
  */
362
- private async navigateTrackAsync(track: Track, url: string | undefined): Promise<void> {
405
+ private async navigateTrackAsync(
406
+ track: Track,
407
+ url: string | undefined
408
+ ): Promise<void> {
363
409
  try {
364
410
  // Handle contextual URL (playable track with queue context)
365
411
  if (url && BrowserPathHelper.isContextual(url)) {
@@ -469,24 +515,49 @@ export class NativeAudioBrowser extends PlaylistPlayer implements AudioBrowserSp
469
515
  // Clear now playing override when track changes (matches Android's PlayerListener.onMediaItemTransition)
470
516
  this.nowPlayingManager.clearNowPlayingOverride()
471
517
 
518
+ // Match Android: load() modifies the queue.
519
+ // If queue is empty, add the track. If queue has items, replace at currentIndex.
520
+ if (this.playlist.length === 0) {
521
+ this.playlist = [track]
522
+ this._currentIndex = 0
523
+ this.onPlaybackQueueChanged(this.playlist)
524
+ } else if (
525
+ this.currentIndex !== undefined &&
526
+ this.playlist[this.currentIndex] !== track
527
+ ) {
528
+ this.playlist[this.currentIndex] = track
529
+ this.onPlaybackQueueChanged(this.playlist)
530
+ }
531
+
472
532
  const lastTrack = this.current
473
533
  const lastPosition = element.currentTime
474
534
  const lastIndex = this.lastIndex
475
535
  const currentIndex = this.currentIndex
476
536
 
537
+ // Set loading flag early so seekTo() calls during async URL resolution
538
+ // are captured as pending seeks rather than silently dropped
539
+ this._loadInProgress = true
540
+ this._pendingSeek = undefined
541
+
477
542
  // Resolve the media URL before loading (async but we don't await)
543
+ const loadId = ++this.currentLoadId
478
544
  const doLoad = async () => {
479
545
  const resolvedTrack: Track = track.src
480
546
  ? { ...track, src: await this.resolveMediaUrl(track.src) }
481
547
  : track
482
548
 
549
+ // A newer load() was called while resolving — discard this stale result
550
+ if (loadId !== this.currentLoadId) {
551
+ return
552
+ }
553
+
483
554
  super.load(resolvedTrack, (loadedTrack) => {
484
555
  this.onPlaybackActiveTrackChanged({
485
556
  lastTrack,
486
557
  lastPosition,
487
558
  lastIndex,
488
559
  index: currentIndex,
489
- track,
560
+ track
490
561
  })
491
562
 
492
563
  // Update now playing metadata
@@ -504,14 +575,13 @@ export class NativeAudioBrowser extends PlaylistPlayer implements AudioBrowserSp
504
575
 
505
576
  // Execute async load without blocking, with error handling
506
577
  doLoad().catch((error: unknown) => {
578
+ this._loadInProgress = false
579
+ this._pendingSeek = undefined
507
580
  console.error('Error loading track:', error)
508
- const message = error instanceof Error ? error.message : 'Failed to load track'
509
- this.onPlaybackError({
510
- error: {
511
- code: 'load-error',
512
- message,
513
- },
514
- })
581
+ const message =
582
+ error instanceof Error ? error.message : 'Failed to load track'
583
+ const playbackError = { code: 'load-error', message }
584
+ this.state = { state: 'error', error: playbackError }
515
585
  })
516
586
  }
517
587
 
@@ -525,7 +595,7 @@ export class NativeAudioBrowser extends PlaylistPlayer implements AudioBrowserSp
525
595
 
526
596
  if (didChange) {
527
597
  this.onPlaybackPlayWhenReadyChanged({
528
- playWhenReady: this._playWhenReady,
598
+ playWhenReady: this._playWhenReady
529
599
  })
530
600
  }
531
601
  }
@@ -553,7 +623,7 @@ export class NativeAudioBrowser extends PlaylistPlayer implements AudioBrowserSp
553
623
 
554
624
  if (didChange) {
555
625
  this.onPlaybackRepeatModeChanged({
556
- repeatMode: mode,
626
+ repeatMode: mode
557
627
  })
558
628
  }
559
629
  }
@@ -619,14 +689,24 @@ export class NativeAudioBrowser extends PlaylistPlayer implements AudioBrowserSp
619
689
  }
620
690
 
621
691
  // MARK: Queue management
622
- setQueue(tracks: Track[], startIndex?: number, startPositionMs?: number): void {
692
+ setQueue(
693
+ tracks: Track[],
694
+ startIndex?: number,
695
+ startPositionMs?: number
696
+ ): void {
623
697
  this.stop()
698
+ // Clear stale references from previous queue
699
+ this.current = undefined
700
+ this._currentIndex = undefined
624
701
  // Hydrate favorites and transform artwork URLs on all tracks in the queue
625
702
  const artworkConfig = this.browserManager.configuration.artwork
626
- this.playlist = tracks.map(track => {
703
+ this.playlist = tracks.map((track) => {
627
704
  try {
628
705
  const hydratedTrack = this.favoriteManager.hydrateFavorite(track)
629
- return RequestConfigBuilder.transformTrackArtwork(hydratedTrack, artworkConfig)
706
+ return RequestConfigBuilder.transformTrackArtwork(
707
+ hydratedTrack,
708
+ artworkConfig
709
+ )
630
710
  } catch (error) {
631
711
  console.error('Failed to transform track:', error)
632
712
  return track // Use original track as fallback
@@ -673,7 +753,7 @@ export class NativeAudioBrowser extends PlaylistPlayer implements AudioBrowserSp
673
753
  // Create updated track with new favorited state
674
754
  const updatedTrack: Track = {
675
755
  ...track,
676
- favorited,
756
+ favorited
677
757
  }
678
758
 
679
759
  // Replace the track in the playlist
@@ -688,7 +768,7 @@ export class NativeAudioBrowser extends PlaylistPlayer implements AudioBrowserSp
688
768
  lastTrack: track,
689
769
  lastPosition: this.element?.currentTime ?? 0,
690
770
  index,
691
- track: updatedTrack,
771
+ track: updatedTrack
692
772
  })
693
773
 
694
774
  // Emit queue changed so useQueue() hook updates
@@ -777,5 +857,4 @@ export class NativeAudioBrowser extends PlaylistPlayer implements AudioBrowserSp
777
857
  openIosOutputPicker(): void {
778
858
  // No-op on web
779
859
  }
780
-
781
860
  }
@@ -14,6 +14,8 @@
14
14
  * - Most specific route wins
15
15
  */
16
16
 
17
+ import { assertedNotNullish } from '../utils/validation'
18
+
17
19
  export interface RouteMatch {
18
20
  params: Record<string, string>
19
21
  specificity: number
@@ -81,13 +83,19 @@ export class SimpleRouter {
81
83
 
82
84
  // Match all segments except the tail wildcard
83
85
  for (let i = 0; i < patternSegments.length - 1; i++) {
84
- if (!this.matchSingleSegment(patternSegments[i]!, pathSegments[i]!, params)) {
86
+ if (
87
+ !this.matchSingleSegment(
88
+ assertedNotNullish(patternSegments[i]),
89
+ assertedNotNullish(pathSegments[i]),
90
+ params
91
+ )
92
+ ) {
85
93
  return null
86
94
  }
87
95
  // Note: The Kotlin implementation doesn't update counts here (appears to be a bug)
88
96
  // but we'll match it exactly for consistency
89
97
  this.updateSegmentCounts(
90
- patternSegments[i]!,
98
+ assertedNotNullish(patternSegments[i]),
91
99
  constantSegments,
92
100
  parameterSegments,
93
101
  wildcardSegments
@@ -106,15 +114,22 @@ export class SimpleRouter {
106
114
  }
107
115
 
108
116
  for (let i = 0; i < patternSegments.length; i++) {
109
- if (!this.matchSingleSegment(patternSegments[i]!, pathSegments[i]!, params)) {
117
+ if (
118
+ !this.matchSingleSegment(
119
+ assertedNotNullish(patternSegments[i]),
120
+ assertedNotNullish(pathSegments[i]),
121
+ params
122
+ )
123
+ ) {
110
124
  return null
111
125
  }
112
- const [constCount, paramCount, wildcardCount] = this.updateSegmentCounts(
113
- patternSegments[i]!,
114
- constantSegments,
115
- parameterSegments,
116
- wildcardSegments
117
- )
126
+ const [constCount, paramCount, wildcardCount] =
127
+ this.updateSegmentCounts(
128
+ assertedNotNullish(patternSegments[i]),
129
+ constantSegments,
130
+ parameterSegments,
131
+ wildcardSegments
132
+ )
118
133
  constantSegments = constCount
119
134
  parameterSegments = paramCount
120
135
  wildcardSegments = wildcardCount
@@ -1,14 +1,14 @@
1
- import { State } from './State'
2
- import type { State as StateType } from './State'
1
+ import type shaka from 'shaka-player/dist/shaka-player.ui'
3
2
  import type {
4
3
  Progress,
5
4
  PartialSetupPlayerOptions,
6
5
  Playback,
7
- PlaybackError,
6
+ PlaybackError
8
7
  } from '../../features'
9
8
  import type { Track } from '../../types'
9
+ import type { State as StateType } from './State'
10
10
  import { SetupNotCalledError } from './SetupNotCalledError'
11
- import type shaka from 'shaka-player/dist/shaka-player.ui'
11
+ import { State } from './State'
12
12
 
13
13
  // Extend Window interface for debug purposes
14
14
  declare global {
@@ -38,6 +38,9 @@ export class Player {
38
38
  protected _current?: Track = undefined
39
39
  protected _playWhenReady = false
40
40
  protected _state: Playback = { state: State.None }
41
+ protected _isStopped = false
42
+ protected _loadInProgress = false
43
+ protected _pendingSeek: number | undefined
41
44
 
42
45
  // current getter/setter
43
46
  public get current(): Track | undefined {
@@ -86,7 +89,7 @@ export class Player {
86
89
  if (this.hasInitialized === true) {
87
90
  const error: PlaybackError = {
88
91
  code: 'player_already_initialized',
89
- message: 'The player has already been initialized via setupPlayer.',
92
+ message: 'The player has already been initialized via setupPlayer.'
90
93
  }
91
94
  // eslint-disable-next-line @typescript-eslint/only-throw-error
92
95
  throw error
@@ -102,8 +105,8 @@ export class Player {
102
105
  state: State.Error,
103
106
  error: {
104
107
  code: 'not_supported',
105
- message: 'Browser not supported...',
106
- },
108
+ message: 'Browser not supported...'
109
+ }
107
110
  }
108
111
  throw new Error('Browser not supported.')
109
112
  }
@@ -152,6 +155,8 @@ export class Player {
152
155
  * event handlers
153
156
  */
154
157
  protected onStateUpdate(state: Exclude<StateType, typeof State.Error>): void {
158
+ // Ignore Shaka/element events while stopped (e.g., from unload)
159
+ if (this._isStopped) return
155
160
  this.state = { state }
156
161
  }
157
162
 
@@ -178,34 +183,33 @@ export class Player {
178
183
 
179
184
  const error: PlaybackError = {
180
185
  code: shakaError.code.toString(),
181
- message: shakaError.message,
186
+ message: shakaError.message
182
187
  }
183
188
 
184
189
  this.state = {
185
190
  state: State.Error,
186
- error,
191
+ error
187
192
  }
188
193
 
189
194
  // Log the error.
190
195
  console.debug('Error code', shakaError.code, 'object', shakaError)
191
196
  }
192
197
 
193
- /**
194
- * NOTE: this method is sync despite the actual load being async. This
195
- * behavior is intentional as it mirrors what happens in Android. State
196
- * changes should be captured by event listeners.
197
- */
198
- public load(track: Track, onComplete?: (track: Track) => void): void {
198
+ public load(track: Track, onLoaded?: (track: Track) => void): void {
199
199
  const player = this.requirePlayer()
200
+ this._isStopped = false
201
+ this._loadInProgress = true
200
202
 
201
203
  if (!track.src) {
204
+ this._loadInProgress = false
205
+ this._pendingSeek = undefined
202
206
  const error: PlaybackError = {
203
207
  code: 'invalid_track',
204
- message: 'Track does not have a valid src URL',
208
+ message: 'Track does not have a valid src URL'
205
209
  }
206
210
  this.state = {
207
211
  state: State.Error,
208
- error,
212
+ error
209
213
  }
210
214
  return
211
215
  }
@@ -213,8 +217,15 @@ export class Player {
213
217
  player
214
218
  .load(track.src)
215
219
  .then(() => {
220
+ this._loadInProgress = false
216
221
  this.current = track
217
- onComplete?.(track)
222
+ onLoaded?.(track)
223
+
224
+ // Execute any pending seek that arrived during loading
225
+ if (this._pendingSeek !== undefined) {
226
+ this.requireElement().currentTime = this._pendingSeek
227
+ this._pendingSeek = undefined
228
+ }
218
229
 
219
230
  // Auto-play if playWhenReady is true
220
231
  if (this.playWhenReady) {
@@ -222,19 +233,23 @@ export class Player {
222
233
  }
223
234
  })
224
235
  .catch((err: unknown) => {
236
+ this._loadInProgress = false
237
+ this._pendingSeek = undefined
225
238
  this.onError(this.toNormalizedError(err))
226
239
  })
227
240
  }
228
241
 
229
- /**
230
- * NOTE: this method is sync despite the actual load being async. This
231
- * behavior is intentional as it mirrors what happens in Android. State
232
- * changes should be captured by event listeners.
233
- */
234
242
  public stop(onComplete?: () => void): void {
235
243
  const player = this.requirePlayer()
236
244
 
237
- this.current = undefined
245
+ // Match Android: stop sets playWhenReady=false and state=stopped,
246
+ // but keeps the current track so play() can resume.
247
+ this._isStopped = true
248
+ this._loadInProgress = false
249
+ this._pendingSeek = undefined
250
+ this.playWhenReady = false
251
+ this.state = { state: State.Stopped }
252
+
238
253
  player
239
254
  .unload()
240
255
  .then(() => onComplete?.())
@@ -244,11 +259,6 @@ export class Player {
244
259
  })
245
260
  }
246
261
 
247
- /**
248
- * NOTE: this method is sync despite the actual load being async. This
249
- * behavior is intentional as it mirrors what happens in Android. State
250
- * changes should be captured by event listeners.
251
- */
252
262
  public play(): void {
253
263
  const element = this.requireElement()
254
264
  this.playWhenReady = true
@@ -258,6 +268,12 @@ export class Player {
258
268
  return
259
269
  }
260
270
 
271
+ // Match Android: play() after stop() re-prepares the current track
272
+ if (this._isStopped && this.current) {
273
+ this.load(this.current)
274
+ return
275
+ }
276
+
261
277
  element.play().catch((err: unknown) => console.error(err))
262
278
  }
263
279
 
@@ -293,11 +309,19 @@ export class Player {
293
309
  }
294
310
 
295
311
  public seekBy(offset: number): void {
312
+ if (this._loadInProgress) {
313
+ this._pendingSeek = (this._pendingSeek ?? 0) + offset
314
+ return
315
+ }
296
316
  const element = this.requireElement()
297
317
  element.currentTime += offset
298
318
  }
299
319
 
300
320
  public seekTo(seconds: number): void {
321
+ if (this._loadInProgress) {
322
+ this._pendingSeek = seconds
323
+ return
324
+ }
301
325
  const element = this.requireElement()
302
326
  element.currentTime = seconds
303
327
  }
@@ -314,10 +338,14 @@ export class Player {
314
338
 
315
339
  public getProgress(): Progress {
316
340
  const element = this.requireElement()
341
+ let buffered = 0
342
+ if (element.buffered.length > 0) {
343
+ buffered = element.buffered.end(element.buffered.length - 1)
344
+ }
317
345
  return {
318
346
  position: element.currentTime,
319
347
  duration: element.duration || 0,
320
- buffered: 0, // TODO: element.buffered.end,
348
+ buffered
321
349
  }
322
350
  }
323
351
  }