gapless.js 2.2.2 → 3.0.1
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 +1 -2
- package/README.md +16 -14
- package/dist/cjs/index.cjs +3 -0
- package/dist/cjs/index.cjs.map +1 -0
- package/dist/index.d.mts +131 -0
- package/dist/index.mjs +2 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +34 -39
- package/src/Queue.ts +289 -0
- package/src/Track.ts +813 -0
- package/src/index.ts +1 -0
- package/CHANGELOG.md +0 -26
- package/index.d.ts +0 -108
- package/index.html +0 -55
- package/index.js +0 -452
- package/tsconfig.json +0 -37
package/src/Queue.ts
ADDED
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import Track from './Track';
|
|
2
|
+
|
|
3
|
+
const PRELOAD_NUM_TRACKS = 2;
|
|
4
|
+
|
|
5
|
+
const isBrowser: boolean = typeof window !== 'undefined';
|
|
6
|
+
const audioContext: AudioContext | null =
|
|
7
|
+
isBrowser && typeof window.AudioContext !== 'undefined' ? new window.AudioContext() : null;
|
|
8
|
+
|
|
9
|
+
// Define interfaces for props and state
|
|
10
|
+
interface QueueProps {
|
|
11
|
+
tracks?: string[];
|
|
12
|
+
onProgress?: (track: Track) => void;
|
|
13
|
+
onEnded?: () => void;
|
|
14
|
+
onPlayNextTrack?: (track: Track | undefined) => void;
|
|
15
|
+
onPlayPreviousTrack?: (track: Track | undefined) => void;
|
|
16
|
+
onStartNewTrack?: (track: Track | undefined) => void;
|
|
17
|
+
webAudioIsDisabled?: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface QueueState {
|
|
21
|
+
volume: number;
|
|
22
|
+
currentTrackIdx: number;
|
|
23
|
+
webAudioIsDisabled: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface AddTrackParams {
|
|
27
|
+
trackUrl: string;
|
|
28
|
+
skipHEAD?: boolean;
|
|
29
|
+
metadata?: Record<string, any>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export default class Queue {
|
|
33
|
+
props: Omit<Required<QueueProps>, 'tracks' | 'webAudioIsDisabled'>; // Make callbacks required but omit others handled differently
|
|
34
|
+
state: QueueState;
|
|
35
|
+
tracks: Track[];
|
|
36
|
+
// Track property is just holding the class itself, which is unusual.
|
|
37
|
+
// If it's meant for instantiation elsewhere, it's fine, but often not needed.
|
|
38
|
+
Track: typeof Track;
|
|
39
|
+
|
|
40
|
+
constructor(props: QueueProps = {}) {
|
|
41
|
+
const {
|
|
42
|
+
tracks = [],
|
|
43
|
+
onProgress = () => {},
|
|
44
|
+
onEnded = () => {},
|
|
45
|
+
onPlayNextTrack = () => {},
|
|
46
|
+
onPlayPreviousTrack = () => {},
|
|
47
|
+
onStartNewTrack = () => {},
|
|
48
|
+
webAudioIsDisabled = false,
|
|
49
|
+
} = props;
|
|
50
|
+
|
|
51
|
+
this.props = {
|
|
52
|
+
onProgress,
|
|
53
|
+
onEnded,
|
|
54
|
+
onPlayNextTrack,
|
|
55
|
+
onPlayPreviousTrack,
|
|
56
|
+
onStartNewTrack,
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
this.state = {
|
|
60
|
+
volume: 1,
|
|
61
|
+
currentTrackIdx: 0,
|
|
62
|
+
webAudioIsDisabled,
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
this.Track = Track; // Assigning the class itself
|
|
66
|
+
|
|
67
|
+
this.tracks = tracks.map(
|
|
68
|
+
(trackUrl: string, idx: number) =>
|
|
69
|
+
new Track({
|
|
70
|
+
trackUrl,
|
|
71
|
+
idx,
|
|
72
|
+
queue: this,
|
|
73
|
+
metadata: {}, // Provide default empty metadata
|
|
74
|
+
})
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
// if the browser doesn't support web audio
|
|
78
|
+
// disable it!
|
|
79
|
+
if (!audioContext) {
|
|
80
|
+
this.disableWebAudio();
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
addTrack({ trackUrl, skipHEAD, metadata = {} }: AddTrackParams): void {
|
|
85
|
+
this.tracks.push(
|
|
86
|
+
new Track({
|
|
87
|
+
trackUrl,
|
|
88
|
+
skipHEAD,
|
|
89
|
+
metadata,
|
|
90
|
+
idx: this.tracks.length,
|
|
91
|
+
queue: this,
|
|
92
|
+
})
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
removeTrack(track: Track): Track[] {
|
|
97
|
+
const index = this.tracks.indexOf(track);
|
|
98
|
+
if (index > -1) {
|
|
99
|
+
return this.tracks.splice(index, 1);
|
|
100
|
+
}
|
|
101
|
+
return [];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
togglePlayPause(): void {
|
|
105
|
+
if (this.currentTrack) this.currentTrack.togglePlayPause();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
play(): void {
|
|
109
|
+
if (this.currentTrack) this.currentTrack.play();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
pause(): void {
|
|
113
|
+
if (this.currentTrack) this.currentTrack.pause();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
playPrevious(): void {
|
|
117
|
+
if (this.currentTrack && this.currentTrack.currentTime > 8) {
|
|
118
|
+
this.currentTrack.seek(0);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
this.resetCurrentTrack();
|
|
123
|
+
|
|
124
|
+
if (--this.state.currentTrackIdx < 0) this.state.currentTrackIdx = 0;
|
|
125
|
+
|
|
126
|
+
// No need to reset again here, play() will handle starting the new current track
|
|
127
|
+
// this.resetCurrentTrack();
|
|
128
|
+
|
|
129
|
+
this.play(); // This will play the new currentTrack
|
|
130
|
+
|
|
131
|
+
if (this.props.onStartNewTrack) this.props.onStartNewTrack(this.currentTrack);
|
|
132
|
+
if (this.props.onPlayPreviousTrack) this.props.onPlayPreviousTrack(this.currentTrack);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
playNext(): void {
|
|
136
|
+
this.resetCurrentTrack(); // Pause and reset the current one
|
|
137
|
+
|
|
138
|
+
// Ensure we don't go beyond the last track index
|
|
139
|
+
if (this.state.currentTrackIdx < this.tracks.length - 1) {
|
|
140
|
+
this.state.currentTrackIdx++;
|
|
141
|
+
} else {
|
|
142
|
+
// Optional: handle queue end (e.g., stop, loop, etc.)
|
|
143
|
+
// For now, just stay on the last track or reset index if looping
|
|
144
|
+
// this.state.currentTrackIdx = 0; // Example: loop back to start
|
|
145
|
+
this.props.onEnded(); // Call the main onEnded callback
|
|
146
|
+
return; // Stop execution if at the end and not looping
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// No need to reset again here
|
|
150
|
+
// this.resetCurrentTrack();
|
|
151
|
+
|
|
152
|
+
this.play(); // Play the new current track
|
|
153
|
+
|
|
154
|
+
if (this.props.onStartNewTrack) this.props.onStartNewTrack(this.currentTrack);
|
|
155
|
+
if (this.props.onPlayNextTrack) this.props.onPlayNextTrack(this.currentTrack);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
resetCurrentTrack(): void {
|
|
159
|
+
if (this.currentTrack) {
|
|
160
|
+
// Check if seek and pause are necessary/safe
|
|
161
|
+
try {
|
|
162
|
+
if (!this.currentTrack.isPaused) {
|
|
163
|
+
this.currentTrack.pause();
|
|
164
|
+
}
|
|
165
|
+
// Only seek if duration is valid
|
|
166
|
+
if (this.currentTrack.duration > 0 && !isNaN(this.currentTrack.duration)) {
|
|
167
|
+
this.currentTrack.seek(0);
|
|
168
|
+
}
|
|
169
|
+
} catch (error) {
|
|
170
|
+
console.error('Error resetting track:', error, this.currentTrack);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
pauseAll(): void {
|
|
176
|
+
// Use forEach for side effects, map is for creating new arrays
|
|
177
|
+
this.tracks.forEach((track: Track) => {
|
|
178
|
+
track.pause();
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
cleanUp(): void {
|
|
183
|
+
// Correctly reference 'track' instead of 'player'
|
|
184
|
+
this.tracks.forEach((track: Track) => {
|
|
185
|
+
// Ensure nodes exist before trying to nullify buffer
|
|
186
|
+
if (track.bufferSourceNode && track.bufferSourceNode.buffer) {
|
|
187
|
+
track.bufferSourceNode.buffer = null; // Release buffer reference
|
|
188
|
+
}
|
|
189
|
+
if (track.audioBuffer) {
|
|
190
|
+
track.audioBuffer = null; // Release internal buffer reference
|
|
191
|
+
}
|
|
192
|
+
// Optional: Stop and disconnect nodes if necessary
|
|
193
|
+
try {
|
|
194
|
+
if (track.bufferSourceNode) {
|
|
195
|
+
track.bufferSourceNode.onended = null; // Remove listener
|
|
196
|
+
track.bufferSourceNode.stop();
|
|
197
|
+
track.bufferSourceNode.disconnect();
|
|
198
|
+
}
|
|
199
|
+
if (track.gainNode && audioContext) {
|
|
200
|
+
track.gainNode.disconnect();
|
|
201
|
+
}
|
|
202
|
+
if (track.audio) {
|
|
203
|
+
track.audio.pause();
|
|
204
|
+
track.audio.src = ''; // Release resource
|
|
205
|
+
track.audio.load();
|
|
206
|
+
track.audio.onended = null;
|
|
207
|
+
track.audio.onerror = null;
|
|
208
|
+
}
|
|
209
|
+
} catch (e) {
|
|
210
|
+
console.error('Error during track cleanup:', e, track);
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
// Consider clearing the tracks array if the queue itself is being destroyed
|
|
214
|
+
// this.tracks = [];
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
gotoTrack(idx: number, playImmediately: boolean = false): void {
|
|
218
|
+
if (idx < 0 || idx >= this.tracks.length) {
|
|
219
|
+
console.warn(`gotoTrack: Index ${idx} out of bounds.`);
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
this.pauseAll(); // Pause potentially playing track
|
|
223
|
+
this.resetCurrentTrack(); // Reset the state of the outgoing track
|
|
224
|
+
|
|
225
|
+
this.state.currentTrackIdx = idx;
|
|
226
|
+
|
|
227
|
+
// Reset the new current track before playing (if needed, though play should handle it)
|
|
228
|
+
// this.resetCurrentTrack(); // Might be redundant if play() handles starting correctly
|
|
229
|
+
|
|
230
|
+
if (playImmediately) {
|
|
231
|
+
this.play();
|
|
232
|
+
if (this.props.onStartNewTrack) this.props.onStartNewTrack(this.currentTrack);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
loadTrack(idx: number, loadHTML5?: boolean): void {
|
|
237
|
+
// only preload if song is within the next PRELOAD_NUM_TRACKS
|
|
238
|
+
if (
|
|
239
|
+
idx < 0 ||
|
|
240
|
+
idx >= this.tracks.length ||
|
|
241
|
+
this.state.currentTrackIdx + PRELOAD_NUM_TRACKS < idx
|
|
242
|
+
)
|
|
243
|
+
return;
|
|
244
|
+
const track = this.tracks[idx];
|
|
245
|
+
|
|
246
|
+
if (track) track.preload(loadHTML5);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
setProps(obj: Partial<Omit<Required<QueueProps>, 'tracks' | 'webAudioIsDisabled'>> = {}): void {
|
|
250
|
+
this.props = { ...this.props, ...obj };
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// These seem redundant if the props callbacks are called directly elsewhere
|
|
254
|
+
// Keep if they add logic, otherwise call props directly
|
|
255
|
+
onEnded(): void {
|
|
256
|
+
if (this.props.onEnded) this.props.onEnded();
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
onProgress(track: Track): void {
|
|
260
|
+
if (this.props.onProgress) this.props.onProgress(track);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
get currentTrack(): Track | undefined {
|
|
264
|
+
return this.tracks[this.state.currentTrackIdx];
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
get nextTrack(): Track | undefined {
|
|
268
|
+
return this.tracks[this.state.currentTrackIdx + 1];
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
disableWebAudio(): void {
|
|
272
|
+
this.state.webAudioIsDisabled = true;
|
|
273
|
+
// Potentially update existing tracks if needed
|
|
274
|
+
this.tracks.forEach((track) => {
|
|
275
|
+
if (track.isUsingWebAudio) {
|
|
276
|
+
// Handle transition back to HTML5 if possible/necessary
|
|
277
|
+
console.warn('Web Audio disabled while track was using it. State might be inconsistent.');
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
setVolume(nextVolume: number): void {
|
|
283
|
+
const clampedVolume = Math.max(0, Math.min(1, nextVolume)); // Clamp between 0 and 1
|
|
284
|
+
|
|
285
|
+
this.state.volume = clampedVolume;
|
|
286
|
+
|
|
287
|
+
this.tracks.forEach((track) => track.setVolume(clampedVolume));
|
|
288
|
+
}
|
|
289
|
+
}
|