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/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
+ }