gapless.js 4.0.0 → 4.0.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gapless.js",
3
- "version": "4.0.0",
3
+ "version": "4.0.2",
4
4
  "description": "Gapless audio playback javascript plugin",
5
5
  "type": "module",
6
6
  "main": "dist/index.mjs",
package/src/Queue.ts CHANGED
@@ -8,10 +8,12 @@ import {
8
8
  setupMediaSession,
9
9
  updateMediaSessionMetadata,
10
10
  updateMediaSessionPlaybackState,
11
+ updateMediaSessionPositionState,
11
12
  } from './utils/mediaSession';
12
13
  import { createQueueMachine } from './machines/queue.machine';
13
14
  import { Track } from './Track';
14
15
  import type { TrackQueueRef } from './Track';
16
+ import { throttle } from './utils/throttle';
15
17
  import type { GaplessOptions, AddTrackOptions, TrackInfo, TrackMetadata } from './types';
16
18
 
17
19
  /** Maximum number of tracks to preload ahead of the current track. */
@@ -37,6 +39,12 @@ export class Queue implements TrackQueueRef {
37
39
  /** Track indices for which a gapless start has been pre-scheduled. */
38
40
  private _scheduledIndices = new Set<number>();
39
41
 
42
+ private _throttledUpdatePositionState = throttle(
43
+ (duration: number, currentTime: number) =>
44
+ updateMediaSessionPositionState(duration, currentTime),
45
+ 1000,
46
+ );
47
+
40
48
  constructor(options: GaplessOptions = {}) {
41
49
  const {
42
50
  tracks = [],
@@ -337,6 +345,9 @@ export class Queue implements TrackQueueRef {
337
345
 
338
346
  onProgress(info: TrackInfo): void {
339
347
  if (info.index !== this._actor.getSnapshot().context.currentTrackIndex) return;
348
+ if (!isNaN(info.duration)) {
349
+ this._throttledUpdatePositionState(info.duration, info.currentTime);
350
+ }
340
351
  this._onProgress?.(info);
341
352
  }
342
353
 
package/src/Track.ts CHANGED
@@ -227,9 +227,10 @@ export class Track {
227
227
 
228
228
  activate(): void {
229
229
  const state = this._actor.getSnapshot().value;
230
- // If the track was in webaudio state (e.g. from gapless scheduling or
231
- // preload BUFFER_READY), reset it so play() can start from a clean state.
232
- if (state === 'webaudio') {
230
+ // If the track was in webaudio or loading state (e.g. from gapless scheduling,
231
+ // preload BUFFER_READY, or stuck in loading), reset it so play() can start
232
+ // from a clean state.
233
+ if (state === 'webaudio' || state === 'loading') {
233
234
  this._stopSourceNode();
234
235
  this._disconnectGain();
235
236
  this.scheduledStartContextTime = null;
@@ -7,6 +7,16 @@
7
7
  // loading Track is preloaded (not yet playing). Decode in progress.
8
8
  // webaudio AudioBufferSourceNode is the active output.
9
9
  //
10
+ // Design invariant — "Web Audio always wins eventually":
11
+ // When a track's buffer finishes decoding (BUFFER_READY), webAudioLoadingState
12
+ // is set to 'LOADED' regardless of what state the machine is in (html5, loading,
13
+ // or idle). We intentionally do NOT switch mid-stream — the track stays in html5
14
+ // until the next play(). But every state handles BUFFER_READY, so the flag is
15
+ // never lost, and the next play() will see the buffer and use Web Audio.
16
+ //
17
+ // All DEACTIVATE transitions land in idle (not loading), so a deactivated track
18
+ // with a loaded buffer is always in idle+LOADED — ready for Web Audio on re-play.
19
+ //
10
20
  // Bug fixes in this rewrite:
11
21
  // #2: BUFFER_READY in html5 stays in html5 (no longer auto-transitions to webaudio)
12
22
  // #3: DEACTIVATE from webaudio → idle (was staying in webaudio)
@@ -83,6 +93,12 @@ export function createTrackMachine(initialContext: TrackContext) {
83
93
  BUFFER_LOADING: {
84
94
  actions: assign({ webAudioLoadingState: () => 'LOADING' as WebAudioLoadingState }),
85
95
  },
96
+ BUFFER_READY: {
97
+ actions: assign({ webAudioLoadingState: () => 'LOADED' as WebAudioLoadingState }),
98
+ },
99
+ BUFFER_ERROR: {
100
+ actions: assign({ webAudioLoadingState: () => 'ERROR' as WebAudioLoadingState }),
101
+ },
86
102
  URL_RESOLVED: {
87
103
  actions: assign({
88
104
  resolvedUrl: ({ event }) => (event as { type: 'URL_RESOLVED'; url: string }).url,
@@ -140,7 +156,7 @@ export function createTrackMachine(initialContext: TrackContext) {
140
156
  }),
141
157
  },
142
158
  DEACTIVATE: {
143
- target: 'loading',
159
+ target: 'idle',
144
160
  actions: assign({ isPlaying: () => false }),
145
161
  },
146
162
  },
@@ -178,6 +194,10 @@ export function createTrackMachine(initialContext: TrackContext) {
178
194
  playbackType: () => 'WEBAUDIO' as PlaybackType,
179
195
  }),
180
196
  },
197
+ DEACTIVATE: {
198
+ target: 'idle',
199
+ actions: assign({ isPlaying: () => false }),
200
+ },
181
201
  URL_RESOLVED: {
182
202
  actions: assign({
183
203
  resolvedUrl: ({ event }) => (event as { type: 'URL_RESOLVED'; url: string }).url,
@@ -53,3 +53,17 @@ export function updateMediaSessionPlaybackState(
53
53
  if (!hasMediaSession) return;
54
54
  navigator.mediaSession.playbackState = isPlaying ? 'playing' : 'paused';
55
55
  }
56
+
57
+ /** Update the OS position/duration display (lock screen scrubber). */
58
+ export function updateMediaSessionPositionState(
59
+ duration: number,
60
+ position: number,
61
+ playbackRate = 1,
62
+ ): void {
63
+ if (!hasMediaSession) return;
64
+ try {
65
+ navigator.mediaSession.setPositionState({ duration, position, playbackRate });
66
+ } catch {
67
+ // Some browsers throw on invalid values (e.g. NaN duration)
68
+ }
69
+ }
@@ -0,0 +1,14 @@
1
+ /** Returns a throttled version of `fn` that fires at most once per `ms`. */
2
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
3
+ export function throttle<T extends (...args: any[]) => void>(
4
+ fn: T,
5
+ ms: number,
6
+ ): (...args: Parameters<T>) => void {
7
+ let last = 0;
8
+ return (...args: Parameters<T>) => {
9
+ const now = performance.now();
10
+ if (now - last < ms) return;
11
+ last = now;
12
+ fn(...args);
13
+ };
14
+ }