gapless 4.0.5
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/LICENSE +21 -0
- package/README.md +163 -0
- package/dist/index.d.ts +220 -0
- package/dist/index.mjs +2 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +61 -0
- package/src/Queue.ts +442 -0
- package/src/Track.ts +490 -0
- package/src/gapless.js +636 -0
- package/src/index.ts +16 -0
- package/src/machines/fetchDecode.machine.ts +130 -0
- package/src/machines/queue.machine.ts +387 -0
- package/src/machines/track.machine.ts +427 -0
- package/src/types.ts +87 -0
- package/src/utils/audioContext.ts +69 -0
- package/src/utils/mediaSession.ts +69 -0
- package/src/utils/throttle.ts +14 -0
package/src/gapless.js
ADDED
|
@@ -0,0 +1,636 @@
|
|
|
1
|
+
(function (factory) {
|
|
2
|
+
// Establish the root object, `window` (`self`) in the browser, or `global` on the server.
|
|
3
|
+
// We use `self` instead of `window` for `WebWorker` support.
|
|
4
|
+
const root =
|
|
5
|
+
(typeof self == 'object' && self.self === self && self) ||
|
|
6
|
+
(typeof global == 'object' && global.global === global && global);
|
|
7
|
+
|
|
8
|
+
// Node.js, CommonJS, or ES6
|
|
9
|
+
if (typeof module === 'object' && typeof module.exports === 'object') {
|
|
10
|
+
module.exports = factory(root, exports);
|
|
11
|
+
// Finally, as a browser global.
|
|
12
|
+
} else {
|
|
13
|
+
root.Gapless = factory(root, {});
|
|
14
|
+
}
|
|
15
|
+
})((root, Gapless) => {
|
|
16
|
+
const PRELOAD_NUM_TRACKS = 2;
|
|
17
|
+
|
|
18
|
+
const isBrowser = typeof window !== 'undefined';
|
|
19
|
+
const audioContext =
|
|
20
|
+
isBrowser && (window.AudioContext || window.webkitAudioContext)
|
|
21
|
+
? new (window.AudioContext || window.webkitAudioContext)()
|
|
22
|
+
: null;
|
|
23
|
+
|
|
24
|
+
const GaplessPlaybackType = {
|
|
25
|
+
HTML5: 'HTML5',
|
|
26
|
+
WEBAUDIO: 'WEBAUDIO',
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const GaplessPlaybackLoadingState = {
|
|
30
|
+
NONE: 'NONE',
|
|
31
|
+
LOADING: 'LOADING',
|
|
32
|
+
LOADED: 'LOADED',
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
class Queue {
|
|
36
|
+
constructor(props = {}) {
|
|
37
|
+
const {
|
|
38
|
+
tracks = [],
|
|
39
|
+
onProgress,
|
|
40
|
+
onEnded,
|
|
41
|
+
onPlayNextTrack,
|
|
42
|
+
onPlayPreviousTrack,
|
|
43
|
+
onStartNewTrack,
|
|
44
|
+
webAudioIsDisabled = false,
|
|
45
|
+
onError,
|
|
46
|
+
onPlayBlocked,
|
|
47
|
+
} = props;
|
|
48
|
+
|
|
49
|
+
this.props = {
|
|
50
|
+
onProgress,
|
|
51
|
+
onEnded,
|
|
52
|
+
onPlayNextTrack,
|
|
53
|
+
onPlayPreviousTrack,
|
|
54
|
+
onStartNewTrack,
|
|
55
|
+
onError,
|
|
56
|
+
onPlayBlocked,
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
this.state = {
|
|
60
|
+
volume: 1,
|
|
61
|
+
currentTrackIdx: 0,
|
|
62
|
+
webAudioIsDisabled,
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
this.Track = Track;
|
|
66
|
+
|
|
67
|
+
this.tracks = tracks.map(
|
|
68
|
+
(trackUrl, idx) =>
|
|
69
|
+
new Track({
|
|
70
|
+
trackUrl,
|
|
71
|
+
idx,
|
|
72
|
+
queue: this,
|
|
73
|
+
})
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
// if the browser doesn't support web audio
|
|
77
|
+
// disable it!
|
|
78
|
+
if (!audioContext) {
|
|
79
|
+
this.disableWebAudio();
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
addTrack({ trackUrl, skipHEAD, metadata = {} }) {
|
|
84
|
+
this.tracks.push(
|
|
85
|
+
new Track({
|
|
86
|
+
trackUrl,
|
|
87
|
+
skipHEAD,
|
|
88
|
+
metadata,
|
|
89
|
+
idx: this.tracks.length,
|
|
90
|
+
queue: this,
|
|
91
|
+
})
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
removeTrack(track) {
|
|
96
|
+
const index = this.tracks.indexOf(track);
|
|
97
|
+
return this.tracks.splice(index, 1);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
togglePlayPause() {
|
|
101
|
+
if (this.currentTrack) this.currentTrack.togglePlayPause();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
play() {
|
|
105
|
+
if (this.currentTrack) this.currentTrack.play();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
pause() {
|
|
109
|
+
if (this.currentTrack) this.currentTrack.pause();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
playPrevious() {
|
|
113
|
+
if (this.currentTrack?.currentTime > 8) {
|
|
114
|
+
this.currentTrack.seek(0);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
this.resetCurrentTrack();
|
|
119
|
+
|
|
120
|
+
if (--this.state.currentTrackIdx < 0) this.state.currentTrackIdx = 0;
|
|
121
|
+
|
|
122
|
+
this.resetCurrentTrack();
|
|
123
|
+
|
|
124
|
+
this.play();
|
|
125
|
+
|
|
126
|
+
if (this.props.onStartNewTrack) this.props.onStartNewTrack(this.currentTrack);
|
|
127
|
+
if (this.props.onPlayPreviousTrack) this.props.onPlayPreviousTrack(this.currentTrack);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
playNext() {
|
|
131
|
+
this.resetCurrentTrack();
|
|
132
|
+
|
|
133
|
+
this.state.currentTrackIdx++;
|
|
134
|
+
|
|
135
|
+
this.resetCurrentTrack();
|
|
136
|
+
|
|
137
|
+
this.play();
|
|
138
|
+
|
|
139
|
+
if (this.props.onStartNewTrack) this.props.onStartNewTrack(this.currentTrack);
|
|
140
|
+
if (this.props.onPlayNextTrack) this.props.onPlayNextTrack(this.currentTrack);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
resetCurrentTrack() {
|
|
144
|
+
if (this.currentTrack) {
|
|
145
|
+
this.currentTrack.seek(0);
|
|
146
|
+
this.currentTrack.pause();
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
pauseAll() {
|
|
151
|
+
Object.values(this.tracks).map((track) => {
|
|
152
|
+
track.pause();
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
cleanUp() {
|
|
157
|
+
Object.values(this.tracks).map((track) => {
|
|
158
|
+
if (player.bufferSourceNode && player.bufferSourceNode.buffer && player.audioBuffer) {
|
|
159
|
+
player.bufferSourceNode.buffer = player.audioBuffer = null;
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
gotoTrack(idx, playImmediately = false) {
|
|
165
|
+
this.pauseAll();
|
|
166
|
+
this.state.currentTrackIdx = idx;
|
|
167
|
+
|
|
168
|
+
this.resetCurrentTrack();
|
|
169
|
+
|
|
170
|
+
if (playImmediately) {
|
|
171
|
+
this.play();
|
|
172
|
+
if (this.props.onStartNewTrack) this.props.onStartNewTrack(this.currentTrack);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
loadTrack(idx, loadHTML5) {
|
|
177
|
+
// only preload if song is within the next 2
|
|
178
|
+
if (this.state.currentTrackIdx + PRELOAD_NUM_TRACKS <= idx) return;
|
|
179
|
+
const track = this.tracks[idx];
|
|
180
|
+
|
|
181
|
+
if (track) track.preload(loadHTML5);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
setProps(obj = {}) {
|
|
185
|
+
this.props = Object.assign(this.props, obj);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
onEnded() {
|
|
189
|
+
if (this.props.onEnded) this.props.onEnded();
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
onProgress(track) {
|
|
193
|
+
if (this.props.onProgress) this.props.onProgress(track);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
get currentTrack() {
|
|
197
|
+
return this.tracks[this.state.currentTrackIdx];
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
get nextTrack() {
|
|
201
|
+
return this.tracks[this.state.currentTrackIdx + 1];
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
disableWebAudio() {
|
|
205
|
+
this.state.webAudioIsDisabled = true;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
setVolume(nextVolume) {
|
|
209
|
+
if (nextVolume < 0) nextVolume = 0;
|
|
210
|
+
else if (nextVolume > 1) nextVolume = 1;
|
|
211
|
+
|
|
212
|
+
this.state.volume = nextVolume;
|
|
213
|
+
|
|
214
|
+
this.tracks.map((track) => track.setVolume(nextVolume));
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
onError() {
|
|
218
|
+
if (this.props.onError) this.props.onError();
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
onPlayBlocked() {
|
|
222
|
+
if (this.props.onPlayBlocked) this.props.onPlayBlocked();
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
resumeAudioContext() {
|
|
226
|
+
if (audioContext && audioContext.state === 'suspended') {
|
|
227
|
+
return audioContext.resume();
|
|
228
|
+
}
|
|
229
|
+
return Promise.resolve();
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
class Track {
|
|
234
|
+
constructor({ trackUrl, skipHEAD, queue, idx, metadata }) {
|
|
235
|
+
// playback type state
|
|
236
|
+
this.playbackType = GaplessPlaybackType.HTML5;
|
|
237
|
+
this.webAudioLoadingState = GaplessPlaybackLoadingState.NONE;
|
|
238
|
+
this.loadedHEAD = false;
|
|
239
|
+
|
|
240
|
+
// basic inputs from Queue
|
|
241
|
+
this.idx = idx;
|
|
242
|
+
this.queue = queue;
|
|
243
|
+
this.trackUrl = trackUrl;
|
|
244
|
+
this.skipHEAD = skipHEAD;
|
|
245
|
+
this.metadata = metadata;
|
|
246
|
+
|
|
247
|
+
this.onEnded = this.onEnded.bind(this);
|
|
248
|
+
this.onProgress = this.onProgress.bind(this);
|
|
249
|
+
|
|
250
|
+
// HTML5 Audio
|
|
251
|
+
this.audio = new Audio();
|
|
252
|
+
this.audio.onerror = this.audioOnError;
|
|
253
|
+
this.audio.onended = () => this.onEnded('HTML5');
|
|
254
|
+
this.audio.controls = false;
|
|
255
|
+
this.audio.volume = queue.state.volume;
|
|
256
|
+
this.audio.preload = 'none';
|
|
257
|
+
this.audio.src = trackUrl;
|
|
258
|
+
// this.audio.onprogress = () => this.debug(this.idx, this.audio.buffered)
|
|
259
|
+
|
|
260
|
+
if (queue.state.webAudioIsDisabled) return;
|
|
261
|
+
|
|
262
|
+
// WebAudio
|
|
263
|
+
this.audioContext = audioContext;
|
|
264
|
+
this.gainNode = this.audioContext ? this.audioContext.createGain() : null;
|
|
265
|
+
this.gainNode.gain.value = queue.state.volume;
|
|
266
|
+
this.webAudioStartedPlayingAt = 0;
|
|
267
|
+
this.webAudioPausedDuration = 0;
|
|
268
|
+
this.webAudioPausedAt = 0;
|
|
269
|
+
this.audioBuffer = null;
|
|
270
|
+
|
|
271
|
+
this.bufferSourceNode = this.audioContext ? this.audioContext.createBufferSource() : null;
|
|
272
|
+
this.bufferSourceNode.onended = this.onEnded;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// private functions
|
|
276
|
+
loadHEAD(cb) {
|
|
277
|
+
if (this.loadedHEAD) return cb();
|
|
278
|
+
|
|
279
|
+
const options = {
|
|
280
|
+
method: 'HEAD',
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
fetch(this.trackUrl, options).then((res) => {
|
|
284
|
+
if (res.redirected) {
|
|
285
|
+
this.trackUrl = res.url;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
this.loadedHEAD = true;
|
|
289
|
+
|
|
290
|
+
cb();
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
loadBuffer(cb) {
|
|
295
|
+
if (this.webAudioLoadingState !== GaplessPlaybackLoadingState.NONE) return;
|
|
296
|
+
|
|
297
|
+
this.webAudioLoadingState = GaplessPlaybackLoadingState.LOADING;
|
|
298
|
+
|
|
299
|
+
fetch(this.trackUrl)
|
|
300
|
+
.then((res) => res.arrayBuffer())
|
|
301
|
+
.then((res) =>
|
|
302
|
+
this.audioContext.decodeAudioData(
|
|
303
|
+
res,
|
|
304
|
+
(buffer) => {
|
|
305
|
+
this.debug('finished downloading track');
|
|
306
|
+
|
|
307
|
+
this.webAudioLoadingState = GaplessPlaybackLoadingState.LOADED;
|
|
308
|
+
|
|
309
|
+
this.bufferSourceNode.buffer = this.audioBuffer = buffer;
|
|
310
|
+
this.bufferSourceNode.connect(this.gainNode);
|
|
311
|
+
|
|
312
|
+
// try to preload next track
|
|
313
|
+
this.queue.loadTrack(this.idx + 1);
|
|
314
|
+
|
|
315
|
+
// if we loaded the active track, switch to web audio
|
|
316
|
+
if (this.isActiveTrack) this.switchToWebAudio();
|
|
317
|
+
// if its not.. then just turn on web audio
|
|
318
|
+
else this.playbackType = GaplessPlaybackType.WEBAUDIO;
|
|
319
|
+
|
|
320
|
+
cb && cb(buffer);
|
|
321
|
+
},
|
|
322
|
+
(err) => {
|
|
323
|
+
console.error('error decoding audio data', err);
|
|
324
|
+
}
|
|
325
|
+
)
|
|
326
|
+
)
|
|
327
|
+
.catch((e) => this.debug('caught fetch error', e));
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
switchToWebAudio(forcePause) {
|
|
331
|
+
// if we've switched tracks, don't switch to web audio
|
|
332
|
+
if (!this.isActiveTrack && !forcePause) return;
|
|
333
|
+
|
|
334
|
+
this.debug(
|
|
335
|
+
'switch to web audio',
|
|
336
|
+
this.currentTime,
|
|
337
|
+
this.isPaused,
|
|
338
|
+
this.audio.duration,
|
|
339
|
+
this.audioBuffer.duration,
|
|
340
|
+
this.audio.duration - this.audioBuffer.duration
|
|
341
|
+
);
|
|
342
|
+
|
|
343
|
+
if (this.currentTime !== 0 && isNaN(this.audio.duration)) {
|
|
344
|
+
this.debug('For some reason this.audio.duration === NaN. Weird.', this.audio);
|
|
345
|
+
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// if currentTime === 0, this is a new track, so play it
|
|
350
|
+
// otherwise we're hitting this mid-track which may
|
|
351
|
+
// happen in the middle of a paused track
|
|
352
|
+
if (forcePause) {
|
|
353
|
+
this.bufferSourceNode.playbackRate.value = 0;
|
|
354
|
+
this.pause();
|
|
355
|
+
} else {
|
|
356
|
+
this.bufferSourceNode.playbackRate.value = this.currentTime !== 0 && this.isPaused ? 0 : 1;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
this.connectGainNode();
|
|
360
|
+
|
|
361
|
+
this.webAudioStartedPlayingAt = this.audioContext.currentTime - this.currentTime;
|
|
362
|
+
|
|
363
|
+
// slight blip, could be improved
|
|
364
|
+
this.bufferSourceNode.start(0, this.currentTime);
|
|
365
|
+
|
|
366
|
+
if (this.isPaused) {
|
|
367
|
+
this.webAudioPausedAt = this.audioContext.currentTime;
|
|
368
|
+
this.bufferSourceNode.playbackRate.value = 0;
|
|
369
|
+
this.gainNode.disconnect(this.audioContext.destination);
|
|
370
|
+
this.bufferSourceNode.onended = null;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
this.audio.pause();
|
|
374
|
+
|
|
375
|
+
this.playbackType = GaplessPlaybackType.WEBAUDIO;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// public-ish functions
|
|
379
|
+
pause() {
|
|
380
|
+
this.debug('pause');
|
|
381
|
+
if (this.isUsingWebAudio) {
|
|
382
|
+
if (this.bufferSourceNode.playbackRate.value === 0) return;
|
|
383
|
+
this.webAudioPausedAt = this.audioContext.currentTime;
|
|
384
|
+
this.bufferSourceNode.playbackRate.value = 0;
|
|
385
|
+
try {
|
|
386
|
+
this.gainNode.disconnect(this.audioContext.destination);
|
|
387
|
+
} catch (err) {
|
|
388
|
+
console.error(err);
|
|
389
|
+
}
|
|
390
|
+
this.bufferSourceNode.onended = null;
|
|
391
|
+
} else {
|
|
392
|
+
this.audio.pause();
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
play() {
|
|
397
|
+
this.debug('play');
|
|
398
|
+
if (this.audioBuffer) {
|
|
399
|
+
// if we've already set up the buffer just set playbackRate to 1
|
|
400
|
+
if (this.isUsingWebAudio) {
|
|
401
|
+
if (this.bufferSourceNode.playbackRate.value === 1) return;
|
|
402
|
+
|
|
403
|
+
if (this.webAudioPausedAt) {
|
|
404
|
+
this.webAudioPausedDuration += this.audioContext.currentTime - this.webAudioPausedAt;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// use seek to avoid bug where track wouldn't play properly
|
|
408
|
+
// if paused for longer than length of track
|
|
409
|
+
// TODO: fix bug -- must be related to bufferSourceNode
|
|
410
|
+
if (this.currentTime !== 0) {
|
|
411
|
+
this.seek(this.currentTime);
|
|
412
|
+
}
|
|
413
|
+
// was paused, now force play
|
|
414
|
+
this.connectGainNode();
|
|
415
|
+
this.bufferSourceNode.playbackRate.value = 1;
|
|
416
|
+
if (!this.bufferSourceNode.onended) {
|
|
417
|
+
this.bufferSourceNode.onended = () => this.onEnded('webaudio3');
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
this.webAudioPausedAt = 0;
|
|
421
|
+
}
|
|
422
|
+
// otherwise set the bufferSourceNode buffer and switch to WebAudio
|
|
423
|
+
else {
|
|
424
|
+
this.switchToWebAudio();
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// Try to preload the next track
|
|
428
|
+
this.queue.loadTrack(this.idx + 1);
|
|
429
|
+
} else {
|
|
430
|
+
this.audio.preload = 'auto';
|
|
431
|
+
const playPromise = this.audio.play();
|
|
432
|
+
if (playPromise && typeof playPromise.catch === 'function') {
|
|
433
|
+
playPromise.catch((err) => {
|
|
434
|
+
if (err.name === 'NotAllowedError') {
|
|
435
|
+
this.queue.onPlayBlocked();
|
|
436
|
+
}
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
if (!this.queue.state.webAudioIsDisabled) {
|
|
440
|
+
if (this.skipHEAD) {
|
|
441
|
+
this.loadBuffer();
|
|
442
|
+
} else {
|
|
443
|
+
this.loadHEAD(() => this.loadBuffer());
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
this.onProgress();
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
togglePlayPause() {
|
|
452
|
+
this.isPaused ? this.play() : this.pause();
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
preload(HTML5) {
|
|
456
|
+
if (HTML5 && this.audio.preload !== 'auto') {
|
|
457
|
+
this.debug('preload', HTML5);
|
|
458
|
+
this.audio.preload = 'auto';
|
|
459
|
+
} else if (!this.audioBuffer && !this.queue.state.webAudioIsDisabled) {
|
|
460
|
+
this.debug('preload', HTML5);
|
|
461
|
+
|
|
462
|
+
if (this.skipHEAD) {
|
|
463
|
+
this.loadBuffer();
|
|
464
|
+
} else {
|
|
465
|
+
this.loadHEAD(() => this.loadBuffer());
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// TODO: add checks for to > duration or null or negative (duration - to)
|
|
471
|
+
seek(to = 0) {
|
|
472
|
+
this.debug('seek', to);
|
|
473
|
+
if (this.isUsingWebAudio) {
|
|
474
|
+
this.seekBufferSourceNode(to);
|
|
475
|
+
} else {
|
|
476
|
+
this.audio.currentTime = to;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
this.onProgress();
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
seekBufferSourceNode(to) {
|
|
483
|
+
const wasPaused = this.isPaused;
|
|
484
|
+
this.bufferSourceNode.onended = null;
|
|
485
|
+
try {
|
|
486
|
+
this.bufferSourceNode.stop();
|
|
487
|
+
} catch (e) {
|
|
488
|
+
console.error(e);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
this.bufferSourceNode = this.audioContext.createBufferSource();
|
|
492
|
+
|
|
493
|
+
this.bufferSourceNode.buffer = this.audioBuffer;
|
|
494
|
+
this.bufferSourceNode.connect(this.gainNode);
|
|
495
|
+
this.bufferSourceNode.onended = () => this.onEnded('webaudio2');
|
|
496
|
+
|
|
497
|
+
this.webAudioStartedPlayingAt = this.audioContext.currentTime - to;
|
|
498
|
+
this.webAudioPausedDuration = 0;
|
|
499
|
+
|
|
500
|
+
this.bufferSourceNode.start(0, to);
|
|
501
|
+
if (wasPaused) {
|
|
502
|
+
this.connectGainNode();
|
|
503
|
+
this.pause();
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
connectGainNode() {
|
|
508
|
+
this.gainNode.connect(this.audioContext.destination);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// basic event handlers
|
|
512
|
+
audioOnError = (e) => {
|
|
513
|
+
this.debug('audioOnError', e);
|
|
514
|
+
this.queue.onError();
|
|
515
|
+
};
|
|
516
|
+
|
|
517
|
+
onEnded(from) {
|
|
518
|
+
this.debug('onEnded', from, this.isActiveTrack, this);
|
|
519
|
+
|
|
520
|
+
// debug: try clearing the onended callback
|
|
521
|
+
if (this.bufferSourceNode && this.bufferSourceNode.onended) {
|
|
522
|
+
this.bufferSourceNode.onended = null;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
this.queue.playNext();
|
|
526
|
+
this.queue.onEnded();
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
onProgress() {
|
|
530
|
+
if (!this.isActiveTrack) return;
|
|
531
|
+
|
|
532
|
+
const isWithinLastTwentyFiveSeconds = this.duration - this.currentTime <= 25;
|
|
533
|
+
const nextTrack = this.queue.nextTrack;
|
|
534
|
+
|
|
535
|
+
// if in last 25 seconds and next track hasn't loaded yet
|
|
536
|
+
// start loading next track's HTML5
|
|
537
|
+
if (isWithinLastTwentyFiveSeconds && nextTrack && !nextTrack.isLoaded) {
|
|
538
|
+
this.queue.loadTrack(this.idx + 1, true);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
this.queue.onProgress(this);
|
|
542
|
+
|
|
543
|
+
// if we're paused, we still want to send one final onProgress call
|
|
544
|
+
// and then bow out, hence this being at the end of the function
|
|
545
|
+
if (this.isPaused) return;
|
|
546
|
+
|
|
547
|
+
// this.debug(this.currentTime, this.duration);
|
|
548
|
+
window.requestAnimationFrame(this.onProgress);
|
|
549
|
+
// setTimeout(this.onProgress, 33.33); // 30fps
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
setVolume(nextVolume) {
|
|
553
|
+
this.audio.volume = nextVolume;
|
|
554
|
+
if (this.gainNode) this.gainNode.gain.value = nextVolume;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// getter helpers
|
|
558
|
+
get isUsingWebAudio() {
|
|
559
|
+
return this.playbackType === GaplessPlaybackType.WEBAUDIO;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
get isPaused() {
|
|
563
|
+
if (this.isUsingWebAudio) {
|
|
564
|
+
return this.bufferSourceNode.playbackRate.value === 0;
|
|
565
|
+
} else {
|
|
566
|
+
return this.audio.paused;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
get currentTime() {
|
|
571
|
+
if (this.isUsingWebAudio) {
|
|
572
|
+
return (
|
|
573
|
+
this.audioContext.currentTime -
|
|
574
|
+
this.webAudioStartedPlayingAt -
|
|
575
|
+
this.webAudioPausedDuration
|
|
576
|
+
);
|
|
577
|
+
} else {
|
|
578
|
+
return this.audio.currentTime;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
get duration() {
|
|
583
|
+
if (this.isUsingWebAudio) {
|
|
584
|
+
return this.audioBuffer.duration;
|
|
585
|
+
} else {
|
|
586
|
+
return this.audio.duration;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
get isActiveTrack() {
|
|
591
|
+
return this.queue.currentTrack === this;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
get isLoaded() {
|
|
595
|
+
return this.webAudioLoadingState === GaplessPlaybackLoadingState.LOADED;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
get state() {
|
|
599
|
+
return {
|
|
600
|
+
playbackType: this.playbackType,
|
|
601
|
+
webAudioLoadingState: this.webAudioLoadingState,
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
get completeState() {
|
|
606
|
+
return {
|
|
607
|
+
playbackType: this.playbackType,
|
|
608
|
+
webAudioLoadingState: this.webAudioLoadingState,
|
|
609
|
+
isPaused: this.isPaused,
|
|
610
|
+
currentTime: this.currentTime,
|
|
611
|
+
duration: this.duration,
|
|
612
|
+
idx: this.idx,
|
|
613
|
+
id: this.metadata.trackId,
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// debug helper
|
|
618
|
+
debug(first, ...args) {
|
|
619
|
+
console.debug(new Date(), `${this.idx}:${first}`, ...args, this.state, this);
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// just a helper to quick jump to the end of a track for testing
|
|
623
|
+
seekToEnd() {
|
|
624
|
+
if (this.isUsingWebAudio) {
|
|
625
|
+
this.seekBufferSourceNode(this.audioBuffer.duration - 6);
|
|
626
|
+
} else {
|
|
627
|
+
this.audio.currentTime = this.audio.duration - 6;
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
Gapless.Queue = Queue;
|
|
633
|
+
Gapless.Track = Track;
|
|
634
|
+
|
|
635
|
+
return Gapless;
|
|
636
|
+
});
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// gapless.js — public API surface
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
|
|
5
|
+
export { Queue } from './Queue';
|
|
6
|
+
export type {
|
|
7
|
+
GaplessOptions,
|
|
8
|
+
AddTrackOptions,
|
|
9
|
+
TrackInfo,
|
|
10
|
+
TrackMetadata,
|
|
11
|
+
PlaybackType,
|
|
12
|
+
WebAudioLoadingState,
|
|
13
|
+
} from './types';
|
|
14
|
+
|
|
15
|
+
// Default export for convenience: import Gapless from 'gapless.js'
|
|
16
|
+
export { Queue as default } from './Queue';
|