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/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2017-2025 Daniel Saewitz
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# gapless.js
|
|
2
|
+
|
|
3
|
+
Gapless audio player for the web. Takes an array of audio tracks and uses HTML5 audio with the Web Audio API to enable seamless, gapless transitions between tracks.
|
|
4
|
+
|
|
5
|
+
Though the earnest goal is not bundle-size driven, it has only one production dependency (xstate) so it operates in a rigid manner according to a well-designed state machine.
|
|
6
|
+
|
|
7
|
+
It has a dead simple API and is easy to get up and running.
|
|
8
|
+
|
|
9
|
+
Built for [Relisten.net](https://relisten.net), where playing back gapless live tracks is paramount.
|
|
10
|
+
|
|
11
|
+
**[Live Demo](https://gapless.saewitz.com)**
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pnpm install gapless
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Quick Start
|
|
20
|
+
|
|
21
|
+
```javascript
|
|
22
|
+
import Queue from 'gapless';
|
|
23
|
+
|
|
24
|
+
const player = new Queue({
|
|
25
|
+
tracks: [
|
|
26
|
+
'https://example.com/track1.mp3',
|
|
27
|
+
'https://example.com/track2.mp3',
|
|
28
|
+
'https://example.com/track3.mp3',
|
|
29
|
+
],
|
|
30
|
+
onProgress: (track) => {
|
|
31
|
+
console.log(`${track.currentTime} / ${track.duration}`);
|
|
32
|
+
},
|
|
33
|
+
onEnded: () => {
|
|
34
|
+
console.log('Queue finished');
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
player.play();
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## API
|
|
42
|
+
|
|
43
|
+
### Constructor Options (`GaplessOptions`)
|
|
44
|
+
|
|
45
|
+
```typescript
|
|
46
|
+
const player = new Queue({
|
|
47
|
+
tracks: [], // Initial list of track URLs
|
|
48
|
+
onProgress: (info) => {}, // Called at ~60fps while playing
|
|
49
|
+
onEnded: () => {}, // Called when the last track ends
|
|
50
|
+
onPlayNextTrack: (info) => {}, // Called when advancing to next track
|
|
51
|
+
onPlayPreviousTrack: (info) => {},// Called when going to previous track
|
|
52
|
+
onStartNewTrack: (info) => {}, // Called whenever a new track becomes current
|
|
53
|
+
onError: (error) => {}, // Called on audio errors
|
|
54
|
+
onPlayBlocked: () => {}, // Called when autoplay is blocked by the browser
|
|
55
|
+
onDebug: (msg) => {}, // Internal debug messages (development only)
|
|
56
|
+
webAudioIsDisabled: false, // Disable Web Audio API (disables gapless playback)
|
|
57
|
+
trackMetadata: [], // Per-track metadata (aligned by index)
|
|
58
|
+
volume: 1, // Initial volume, 0.0–1.0
|
|
59
|
+
});
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Methods
|
|
63
|
+
|
|
64
|
+
| Method | Description |
|
|
65
|
+
|--------|-------------|
|
|
66
|
+
| `play()` | Start or resume playback |
|
|
67
|
+
| `pause()` | Pause playback |
|
|
68
|
+
| `togglePlayPause()` | Toggle between play and pause |
|
|
69
|
+
| `next()` | Advance to the next track |
|
|
70
|
+
| `previous()` | Go to previous track (restarts current track if > 8s in) |
|
|
71
|
+
| `gotoTrack(index, playImmediately?)` | Jump to a track by index |
|
|
72
|
+
| `seek(time)` | Seek to a position in seconds |
|
|
73
|
+
| `setVolume(volume)` | Set volume (0.0–1.0) |
|
|
74
|
+
| `addTrack(url, options?)` | Add a track to the end of the queue |
|
|
75
|
+
| `removeTrack(index)` | Remove a track by index |
|
|
76
|
+
| `resumeAudioContext()` | Resume the AudioContext (for browsers that require user gesture) |
|
|
77
|
+
| `destroy()` | Clean up all resources |
|
|
78
|
+
|
|
79
|
+
### Getters
|
|
80
|
+
|
|
81
|
+
| Getter | Type | Description |
|
|
82
|
+
|--------|------|-------------|
|
|
83
|
+
| `currentTrack` | `TrackInfo \| undefined` | Snapshot of the current track |
|
|
84
|
+
| `currentTrackIndex` | `number` | Index of the current track |
|
|
85
|
+
| `tracks` | `readonly TrackInfo[]` | Snapshot of all tracks |
|
|
86
|
+
| `isPlaying` | `boolean` | Whether the queue is playing |
|
|
87
|
+
| `isPaused` | `boolean` | Whether the queue is paused |
|
|
88
|
+
| `volume` | `number` | Current volume |
|
|
89
|
+
|
|
90
|
+
### `TrackInfo`
|
|
91
|
+
|
|
92
|
+
All callbacks and getters return `TrackInfo` objects — plain data snapshots with no methods:
|
|
93
|
+
|
|
94
|
+
```typescript
|
|
95
|
+
interface TrackInfo {
|
|
96
|
+
index: number; // Position in the queue
|
|
97
|
+
currentTime: number; // Playback position in seconds
|
|
98
|
+
duration: number; // Total duration (NaN until loaded)
|
|
99
|
+
isPlaying: boolean;
|
|
100
|
+
isPaused: boolean;
|
|
101
|
+
volume: number;
|
|
102
|
+
trackUrl: string; // Resolved audio URL
|
|
103
|
+
playbackType: 'HTML5' | 'WEBAUDIO';
|
|
104
|
+
webAudioLoadingState: 'NONE' | 'LOADING' | 'LOADED' | 'ERROR';
|
|
105
|
+
metadata?: TrackMetadata;
|
|
106
|
+
machineState: string; // Internal state machine state
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### `AddTrackOptions`
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
player.addTrack('https://example.com/track.mp3', {
|
|
114
|
+
skipHEAD: true, // Skip HEAD request for URL resolution
|
|
115
|
+
metadata: {
|
|
116
|
+
title: 'Track Title',
|
|
117
|
+
artist: 'Artist',
|
|
118
|
+
album: 'Album',
|
|
119
|
+
artwork: [{ src: 'https://example.com/art.jpg', sizes: '512x512', type: 'image/jpeg' }],
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### `TrackMetadata`
|
|
125
|
+
|
|
126
|
+
Metadata is used for the [Media Session API](https://developer.mozilla.org/en-US/docs/Web/API/Media_Session_API) (lock screen controls, browser media UI) and can contain arbitrary additional fields:
|
|
127
|
+
|
|
128
|
+
```typescript
|
|
129
|
+
interface TrackMetadata {
|
|
130
|
+
title?: string;
|
|
131
|
+
artist?: string;
|
|
132
|
+
album?: string;
|
|
133
|
+
artwork?: MediaImage[];
|
|
134
|
+
[key: string]: unknown;
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Migration from v3
|
|
139
|
+
|
|
140
|
+
v4 is a complete rewrite. The public API has changed:
|
|
141
|
+
|
|
142
|
+
| v3 | v4 |
|
|
143
|
+
|----|-----|
|
|
144
|
+
| `import GaplessQueue from 'gapless.js'` | `import Queue from 'gapless'` (or `import { Queue }`) |
|
|
145
|
+
| `player.playNext()` | `player.next()` |
|
|
146
|
+
| `player.playPrevious()` | `player.previous()` |
|
|
147
|
+
| `player.resetCurrentTrack()` | `player.seek(0)` |
|
|
148
|
+
| `player.disableWebAudio()` | Pass `webAudioIsDisabled: true` in constructor |
|
|
149
|
+
| `player.nextTrack` | `player.tracks[player.currentTrackIndex + 1]` |
|
|
150
|
+
| `track.completeState` | Callbacks now receive `TrackInfo` objects |
|
|
151
|
+
| Callbacks receive Track instances | Callbacks receive plain `TrackInfo` data snapshots |
|
|
152
|
+
|
|
153
|
+
### Key differences
|
|
154
|
+
|
|
155
|
+
- **State machines**: Internally uses [XState](https://xstate.js.org/) for queue and track state management. XState is bundled — no extra dependency needed.
|
|
156
|
+
- **ESM only**: Published as ES module only. No CommonJS build.
|
|
157
|
+
- **TrackInfo**: All callbacks and getters return plain data objects (`TrackInfo`) instead of Track class instances.
|
|
158
|
+
- **Media Session**: Built-in support for the Media Session API via `trackMetadata`.
|
|
159
|
+
- **Volume**: Volume is now set via `setVolume(n)` and readable via the `volume` getter.
|
|
160
|
+
|
|
161
|
+
## License
|
|
162
|
+
|
|
163
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
type PlaybackType = 'HTML5' | 'WEBAUDIO';
|
|
2
|
+
type WebAudioLoadingState = 'NONE' | 'LOADING' | 'LOADED' | 'ERROR';
|
|
3
|
+
/** Metadata attached to a track (arbitrary user data). */
|
|
4
|
+
interface TrackMetadata {
|
|
5
|
+
title?: string;
|
|
6
|
+
artist?: string;
|
|
7
|
+
album?: string;
|
|
8
|
+
artwork?: MediaImage[];
|
|
9
|
+
[key: string]: unknown;
|
|
10
|
+
}
|
|
11
|
+
/** Options accepted by the Queue constructor. */
|
|
12
|
+
interface GaplessOptions {
|
|
13
|
+
/** Initial list of track URLs. */
|
|
14
|
+
tracks?: string[];
|
|
15
|
+
/** Called at ~60fps while playing. */
|
|
16
|
+
onProgress?: (info: TrackInfo) => void;
|
|
17
|
+
/** Called when the last track in the queue ends. */
|
|
18
|
+
onEnded?: () => void;
|
|
19
|
+
/** Called when the queue advances to the next track. */
|
|
20
|
+
onPlayNextTrack?: (info: TrackInfo) => void;
|
|
21
|
+
/** Called when the queue goes back to the previous track. */
|
|
22
|
+
onPlayPreviousTrack?: (info: TrackInfo) => void;
|
|
23
|
+
/** Called whenever a new track becomes the current track. */
|
|
24
|
+
onStartNewTrack?: (info: TrackInfo) => void;
|
|
25
|
+
/** Called on HTML5 audio errors. */
|
|
26
|
+
onError?: (error: Error) => void;
|
|
27
|
+
/** Called with internal debug messages. Only use for development. */
|
|
28
|
+
onDebug?: (msg: string) => void;
|
|
29
|
+
/** Called when autoplay is blocked by the browser. */
|
|
30
|
+
onPlayBlocked?: () => void;
|
|
31
|
+
/** Called whenever the queue state machine transitions. */
|
|
32
|
+
onQueueStateChange?: (snapshot: {
|
|
33
|
+
state: string;
|
|
34
|
+
context: {
|
|
35
|
+
currentTrackIndex: number;
|
|
36
|
+
trackCount: number;
|
|
37
|
+
};
|
|
38
|
+
}) => void;
|
|
39
|
+
/**
|
|
40
|
+
* Set true to disable Web Audio API entirely and use HTML5 audio only.
|
|
41
|
+
* Gapless playback will not be available in this mode.
|
|
42
|
+
*/
|
|
43
|
+
webAudioIsDisabled?: boolean;
|
|
44
|
+
/** Per-track metadata (aligned to the tracks array by index). */
|
|
45
|
+
trackMetadata?: TrackMetadata[];
|
|
46
|
+
/** Initial volume, 0.0–1.0. Defaults to 1. */
|
|
47
|
+
volume?: number;
|
|
48
|
+
}
|
|
49
|
+
/** Options for dynamically adding a track. */
|
|
50
|
+
interface AddTrackOptions {
|
|
51
|
+
/**
|
|
52
|
+
* Skip the HEAD request used to resolve redirects.
|
|
53
|
+
* Set true when the URL is already a direct, final URL.
|
|
54
|
+
*/
|
|
55
|
+
skipHEAD?: boolean;
|
|
56
|
+
metadata?: TrackMetadata;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* A plain-data snapshot of a track's current state.
|
|
60
|
+
* Returned by Queue getters and passed to callbacks.
|
|
61
|
+
* No methods — pure data.
|
|
62
|
+
*/
|
|
63
|
+
interface TrackInfo {
|
|
64
|
+
/** Zero-based position of this track in the queue. */
|
|
65
|
+
index: number;
|
|
66
|
+
/** Current playback position in seconds. */
|
|
67
|
+
currentTime: number;
|
|
68
|
+
/** Total duration in seconds (NaN until loaded). */
|
|
69
|
+
duration: number;
|
|
70
|
+
/** True if currently playing. */
|
|
71
|
+
isPlaying: boolean;
|
|
72
|
+
/** True if explicitly paused. */
|
|
73
|
+
isPaused: boolean;
|
|
74
|
+
/** Current volume, 0.0–1.0. */
|
|
75
|
+
volume: number;
|
|
76
|
+
/** The resolved URL of the audio file. */
|
|
77
|
+
trackUrl: string;
|
|
78
|
+
/** Which backend is currently producing sound. */
|
|
79
|
+
playbackType: PlaybackType;
|
|
80
|
+
/** Whether the Web Audio buffer has been decoded. */
|
|
81
|
+
webAudioLoadingState: WebAudioLoadingState;
|
|
82
|
+
/** Arbitrary metadata supplied when the track was added. */
|
|
83
|
+
metadata?: TrackMetadata;
|
|
84
|
+
/** Current xstate machine state for this track (e.g. 'idle', 'html5', 'webaudio'). */
|
|
85
|
+
machineState: string;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
interface TrackQueueRef {
|
|
89
|
+
onTrackEnded(track: Track): void;
|
|
90
|
+
onTrackBufferReady(track: Track): void;
|
|
91
|
+
onProgress(info: TrackInfo): void;
|
|
92
|
+
onError(error: Error): void;
|
|
93
|
+
onPlayBlocked(): void;
|
|
94
|
+
onDebug(msg: string): void;
|
|
95
|
+
readonly volume: number;
|
|
96
|
+
readonly webAudioIsDisabled: boolean;
|
|
97
|
+
readonly currentTrackIndex: number;
|
|
98
|
+
}
|
|
99
|
+
declare class Track {
|
|
100
|
+
readonly index: number;
|
|
101
|
+
readonly metadata: TrackMetadata;
|
|
102
|
+
private _trackUrl;
|
|
103
|
+
private _resolvedUrl;
|
|
104
|
+
private readonly skipHEAD;
|
|
105
|
+
/** Temporary holder between fetch and decode steps (unserializable — stays on Track class). */
|
|
106
|
+
private _pendingArrayBuffer;
|
|
107
|
+
readonly audio: HTMLAudioElement;
|
|
108
|
+
private readonly _webAudioDisabled;
|
|
109
|
+
private get ctx();
|
|
110
|
+
private gainNode;
|
|
111
|
+
private sourceNode;
|
|
112
|
+
audioBuffer: AudioBuffer | null;
|
|
113
|
+
/** AudioContext.currentTime when the current source node was started. */
|
|
114
|
+
private webAudioStartedAt;
|
|
115
|
+
/** Track-time (seconds) frozen at the moment of the most recent pause. */
|
|
116
|
+
private pausedAtTrackTime;
|
|
117
|
+
private readonly _actor;
|
|
118
|
+
private readonly queueRef;
|
|
119
|
+
private rafId;
|
|
120
|
+
constructor(opts: {
|
|
121
|
+
trackUrl: string;
|
|
122
|
+
index: number;
|
|
123
|
+
queue: TrackQueueRef;
|
|
124
|
+
skipHEAD?: boolean;
|
|
125
|
+
metadata?: TrackMetadata;
|
|
126
|
+
});
|
|
127
|
+
play(): void;
|
|
128
|
+
pause(): void;
|
|
129
|
+
seek(time: number): void;
|
|
130
|
+
setVolume(v: number): void;
|
|
131
|
+
preload(): void;
|
|
132
|
+
seekToEnd(secondsFromEnd?: number): void;
|
|
133
|
+
activate(): void;
|
|
134
|
+
deactivate(): void;
|
|
135
|
+
destroy(): void;
|
|
136
|
+
cancelGaplessStart(): void;
|
|
137
|
+
scheduleGaplessStart(when: number): void;
|
|
138
|
+
get currentTime(): number;
|
|
139
|
+
get duration(): number;
|
|
140
|
+
get isPaused(): boolean;
|
|
141
|
+
get isPlaying(): boolean;
|
|
142
|
+
get trackUrl(): string;
|
|
143
|
+
get playbackType(): PlaybackType;
|
|
144
|
+
get webAudioLoadingState(): WebAudioLoadingState;
|
|
145
|
+
get hasSourceNode(): boolean;
|
|
146
|
+
get machineState(): string;
|
|
147
|
+
get scheduledStartContextTime(): number | null;
|
|
148
|
+
get isBufferLoaded(): boolean;
|
|
149
|
+
toInfo(): TrackInfo;
|
|
150
|
+
private _playHtml5;
|
|
151
|
+
private _seekHtml5;
|
|
152
|
+
private _startSourceNode;
|
|
153
|
+
private _stopSourceNode;
|
|
154
|
+
private _disconnectGain;
|
|
155
|
+
private _seekWebAudio;
|
|
156
|
+
private _handleWebAudioEnded;
|
|
157
|
+
startProgressLoop(): void;
|
|
158
|
+
private _stopProgressLoop;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
declare class Queue implements TrackQueueRef {
|
|
162
|
+
private _tracks;
|
|
163
|
+
private readonly _actor;
|
|
164
|
+
private readonly _onProgress?;
|
|
165
|
+
private readonly _onEnded?;
|
|
166
|
+
private readonly _onPlayNextTrack?;
|
|
167
|
+
private readonly _onPlayPreviousTrack?;
|
|
168
|
+
private readonly _onStartNewTrack?;
|
|
169
|
+
private readonly _onError?;
|
|
170
|
+
private readonly _onPlayBlocked?;
|
|
171
|
+
private readonly _onQueueStateChange?;
|
|
172
|
+
private readonly _onDebug?;
|
|
173
|
+
readonly webAudioIsDisabled: boolean;
|
|
174
|
+
private _volume;
|
|
175
|
+
/** Index of the next track with a pre-scheduled gapless start, or null. */
|
|
176
|
+
private _scheduledNextIndex;
|
|
177
|
+
private _throttledUpdatePositionState;
|
|
178
|
+
constructor(options?: GaplessOptions);
|
|
179
|
+
play(): void;
|
|
180
|
+
pause(): void;
|
|
181
|
+
togglePlayPause(): void;
|
|
182
|
+
next(): void;
|
|
183
|
+
previous(): void;
|
|
184
|
+
gotoTrack(index: number, playImmediately?: boolean): void;
|
|
185
|
+
seek(time: number): void;
|
|
186
|
+
setVolume(volume: number): void;
|
|
187
|
+
addTrack(url: string, options?: AddTrackOptions): void;
|
|
188
|
+
removeTrack(index: number): void;
|
|
189
|
+
resumeAudioContext(): Promise<void>;
|
|
190
|
+
destroy(): void;
|
|
191
|
+
get currentTrack(): TrackInfo | undefined;
|
|
192
|
+
get currentTrackIndex(): number;
|
|
193
|
+
get tracks(): readonly TrackInfo[];
|
|
194
|
+
get isPlaying(): boolean;
|
|
195
|
+
get isPaused(): boolean;
|
|
196
|
+
get volume(): number;
|
|
197
|
+
/** Snapshot of the queue state machine (state name + context). For debugging. */
|
|
198
|
+
get queueSnapshot(): {
|
|
199
|
+
state: string;
|
|
200
|
+
context: {
|
|
201
|
+
currentTrackIndex: number;
|
|
202
|
+
trackCount: number;
|
|
203
|
+
};
|
|
204
|
+
};
|
|
205
|
+
onTrackEnded(track: Track): void;
|
|
206
|
+
onTrackBufferReady(track: Track): void;
|
|
207
|
+
onProgress(info: TrackInfo): void;
|
|
208
|
+
onError(error: Error): void;
|
|
209
|
+
onPlayBlocked(): void;
|
|
210
|
+
onDebug(msg: string): void;
|
|
211
|
+
/** Look up a track by index — safe for use inside machine actions. */
|
|
212
|
+
private _trackAt;
|
|
213
|
+
private get _currentTrack();
|
|
214
|
+
private _preloadAhead;
|
|
215
|
+
private _cancelScheduledGapless;
|
|
216
|
+
private _tryScheduleGapless;
|
|
217
|
+
private _computeTrackEndTime;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export { type AddTrackOptions, type GaplessOptions, type PlaybackType, Queue, type TrackInfo, type TrackMetadata, type WebAudioLoadingState, Queue as default };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{createActor as Y}from"xstate";var B=typeof window<"u",p;function $(){if(!B)return null;let r=window.AudioContext??window.webkitAudioContext;return r?new r:null}function k(){return p===void 0?null:p}function x(){p===void 0&&(p=$());let r=p;return r&&r.state==="suspended"?r.resume():Promise.resolve()}var T=typeof navigator<"u"&&"mediaSession"in navigator;function v(r){if(!T)return;let{mediaSession:e}=navigator;e.setActionHandler("play",r.onPlay),e.setActionHandler("pause",r.onPause),e.setActionHandler("nexttrack",r.onNext),e.setActionHandler("previoustrack",r.onPrevious),e.setActionHandler("seekto",t=>{t.seekTime!=null&&r.onSeek(t.seekTime)})}function P(r){if(T){if(!r){navigator.mediaSession.metadata=null;return}navigator.mediaSession.metadata=new MediaMetadata({title:r.title??"",artist:r.artist??"",album:r.album??"",artwork:r.artwork??[]})}}function b(r){T&&(navigator.mediaSession.playbackState=r?"playing":"paused")}function m(r,e,t=1){if(T)try{navigator.mediaSession.setPositionState({duration:r,position:e,playbackRate:t})}catch{}}import{setup as F,assign as h}from"xstate";function D(r){return F({types:{context:{},events:{}},guards:{hasNextTrack:({context:e})=>e.currentTrackIndex+1<e.trackCount,playImmediately:({event:e})=>!!e.playImmediately},actions:{incrementTrackCount:h({trackCount:({context:e})=>e.trackCount+1}),decrementTrackCount:h({trackCount:({context:e})=>Math.max(0,e.trackCount-1),currentTrackIndex:({context:e,event:t})=>t.index<e.currentTrackIndex?Math.max(0,e.currentTrackIndex-1):e.currentTrackIndex}),gotoTrackIndex:h({currentTrackIndex:({event:e})=>e.index}),advanceToNextTrack:h({currentTrackIndex:({context:e})=>{let t=e.currentTrackIndex+1;return t<e.trackCount?t:e.currentTrackIndex}}),goToPreviousTrack:h({currentTrackIndex:({context:e})=>Math.max(0,e.currentTrackIndex-1)}),advanceOnTrackEnd:h({currentTrackIndex:({context:e})=>e.currentTrackIndex+1}),resetToFirstTrack:h({currentTrackIndex:()=>0}),deactivateCurrent:()=>{},deactivateEndedTrack:()=>{},activateAndPlayCurrent:()=>{},playOrContinueGapless:()=>{},cancelAllGapless:()=>{},notifyStartNewTrack:()=>{},notifyPlayNextTrack:()=>{},notifyPlayPreviousTrack:()=>{},notifyEnded:()=>{},updateMediaSessionMetadata:()=>{},preloadAhead:()=>{},playCurrent:()=>{},pauseCurrent:()=>{},seekCurrent:()=>{},seekCurrentToZero:()=>{},scheduleGapless:()=>{},cancelScheduledGapless:()=>{},cancelAndRescheduleGapless:()=>{}}}).createMachine({id:"queue",initial:"idle",context:r,on:{ADD_TRACK:{actions:"incrementTrackCount"},REMOVE_TRACK:{actions:"decrementTrackCount"}},states:{idle:{on:{PLAY:{target:"playing",actions:["playCurrent","updateMediaSessionMetadata","preloadAhead","scheduleGapless"]},GOTO:[{guard:"playImmediately",target:"playing",actions:["deactivateCurrent","cancelAllGapless","gotoTrackIndex","activateAndPlayCurrent","notifyStartNewTrack","updateMediaSessionMetadata","preloadAhead"]},{target:"paused",actions:["deactivateCurrent","cancelAllGapless","gotoTrackIndex","seekCurrentToZero","preloadAhead"]}],TRACK_LOADED:{actions:["preloadAhead"]}}},playing:{on:{PAUSE:{target:"paused",actions:["cancelScheduledGapless","pauseCurrent"]},TOGGLE:{target:"paused",actions:["cancelScheduledGapless","pauseCurrent"]},NEXT:{actions:["deactivateCurrent","cancelAllGapless","advanceToNextTrack","activateAndPlayCurrent","notifyStartNewTrack","notifyPlayNextTrack","updateMediaSessionMetadata","preloadAhead"]},PREVIOUS:{actions:["deactivateCurrent","cancelAllGapless","goToPreviousTrack","activateAndPlayCurrent","notifyStartNewTrack","notifyPlayPreviousTrack","updateMediaSessionMetadata","preloadAhead"]},GOTO:[{guard:"playImmediately",actions:["deactivateCurrent","cancelAllGapless","gotoTrackIndex","activateAndPlayCurrent","notifyStartNewTrack","updateMediaSessionMetadata","preloadAhead"]},{actions:["deactivateCurrent","cancelAllGapless","gotoTrackIndex","seekCurrentToZero","preloadAhead"]}],SEEK:{actions:["seekCurrent","cancelAndRescheduleGapless"]},TRACK_ENDED:[{guard:"hasNextTrack",target:"playing",actions:["deactivateEndedTrack","advanceOnTrackEnd","playOrContinueGapless","notifyStartNewTrack","notifyPlayNextTrack","updateMediaSessionMetadata","preloadAhead"]},{target:"ended",actions:["deactivateEndedTrack","notifyEnded"]}],TRACK_LOADED:{actions:["scheduleGapless","preloadAhead"]}}},paused:{on:{PLAY:{target:"playing",actions:["playCurrent","updateMediaSessionMetadata","preloadAhead","scheduleGapless"]},TOGGLE:{target:"playing",actions:["playCurrent","updateMediaSessionMetadata","preloadAhead","scheduleGapless"]},NEXT:{actions:["deactivateCurrent","cancelAllGapless","advanceToNextTrack","activateAndPlayCurrent","notifyStartNewTrack","notifyPlayNextTrack","updateMediaSessionMetadata","preloadAhead"]},PREVIOUS:{actions:["deactivateCurrent","cancelAllGapless","goToPreviousTrack","activateAndPlayCurrent","notifyStartNewTrack","notifyPlayPreviousTrack","updateMediaSessionMetadata","preloadAhead"]},GOTO:[{guard:"playImmediately",target:"playing",actions:["deactivateCurrent","cancelAllGapless","gotoTrackIndex","activateAndPlayCurrent","notifyStartNewTrack","updateMediaSessionMetadata","preloadAhead"]},{actions:["deactivateCurrent","cancelAllGapless","gotoTrackIndex","seekCurrentToZero","preloadAhead"]}],SEEK:{actions:["seekCurrent"]},TRACK_ENDED:[{guard:"hasNextTrack",target:"paused",actions:["deactivateEndedTrack","advanceOnTrackEnd","notifyStartNewTrack","updateMediaSessionMetadata"]},{target:"ended",actions:["deactivateEndedTrack","notifyEnded"]}],TRACK_LOADED:{actions:["preloadAhead"]}}},ended:{on:{PLAY:{target:"playing",actions:["resetToFirstTrack","playCurrent","updateMediaSessionMetadata","preloadAhead","scheduleGapless"]},GOTO:[{guard:"playImmediately",target:"playing",actions:["deactivateCurrent","cancelAllGapless","gotoTrackIndex","activateAndPlayCurrent","notifyStartNewTrack","updateMediaSessionMetadata","preloadAhead"]},{target:"paused",actions:["deactivateCurrent","cancelAllGapless","gotoTrackIndex","seekCurrentToZero","preloadAhead"]}]}}}})}import{createActor as V,fromPromise as S}from"xstate";import{setup as q,assign as d}from"xstate";import{setup as W,assign as C,sendParent as y,fromPromise as E}from"xstate";var A=W({types:{context:{},input:{}},actors:{resolveUrl:E(async()=>null),fetchAudio:E(async()=>{}),decodeAudio:E(async()=>{})},guards:{shouldSkipHEAD:({context:r})=>r.skipHEAD}}).createMachine({id:"fetchDecode",initial:"resolvingUrl",context:({input:r})=>({trackUrl:r.trackUrl,resolvedUrl:r.resolvedUrl,skipHEAD:r.skipHEAD}),states:{resolvingUrl:{always:{guard:"shouldSkipHEAD",target:"fetching"},invoke:{id:"resolveUrl",src:"resolveUrl",input:({context:r})=>({trackUrl:r.trackUrl}),onDone:{target:"fetching",actions:[C({resolvedUrl:({event:r,context:e})=>r.output??e.resolvedUrl,skipHEAD:()=>!0}),y(({event:r,context:e})=>({type:"URL_RESOLVED",url:r.output??e.resolvedUrl}))]},onError:{target:"fetching",actions:C({skipHEAD:()=>!0})}}},fetching:{invoke:{id:"fetchAudio",src:"fetchAudio",input:({context:r})=>({resolvedUrl:r.resolvedUrl}),onDone:"decoding",onError:{target:"error",actions:y({type:"BUFFER_ERROR"})}}},decoding:{invoke:{id:"decodeAudio",src:"decodeAudio",input:()=>{},onDone:{target:"done",actions:y({type:"BUFFER_READY"})},onError:{target:"error",actions:y({type:"BUFFER_ERROR"})}}},done:{type:"final"},error:{type:"final"}}});function N(r){return q({types:{context:{},events:{}},actors:{fetchDecode:A},guards:{canPlayWebAudio:()=>!1,canStartFetch:({context:e})=>e.webAudioLoadingState==="NONE"&&e.fetchDecodeRef===null},actions:{playHtml5:()=>{},startSourceNode:()=>{},startScheduledSourceNode:()=>{},startProgressLoop:()=>{},pauseHtml5:()=>{},freezePausedTime:()=>{},stopSourceNode:()=>{},disconnectGain:()=>{},stopProgressLoop:()=>{},reportProgress:()=>{},seekHtml5:()=>{},seekWebAudio:()=>{},resetHtml5Element:()=>{},resetTiming:()=>{},notifyTrackEnded:()=>{},setIsPlaying:d({isPlaying:()=>!0}),clearIsPlaying:d({isPlaying:()=>!1}),setLoadingState:d({webAudioLoadingState:()=>"LOADING"}),setLoadedState:d({webAudioLoadingState:()=>"LOADED"}),setErrorState:d({webAudioLoadingState:()=>"ERROR"}),clearScheduleAndLookahead:d({scheduledStartContextTime:()=>null,notifiedLookahead:()=>!1}),setPlayingWebAudio:d({isPlaying:()=>!0,webAudioLoadingState:()=>"LOADED",playbackType:()=>"WEBAUDIO"}),setScheduledGapless:d({isPlaying:()=>!0,webAudioLoadingState:()=>"LOADED",playbackType:()=>"WEBAUDIO",scheduledStartContextTime:({event:e})=>e.when}),clearPlayingAndSchedule:d({isPlaying:()=>!1,scheduledStartContextTime:()=>null,notifiedLookahead:()=>!1}),setNotifiedLookahead:d({notifiedLookahead:()=>!0}),setResolvedUrl:d({resolvedUrl:({event:e})=>e.url}),clearScheduledStart:d({scheduledStartContextTime:()=>null}),setPlayingWebAudioType:d({isPlaying:()=>!0,playbackType:()=>"WEBAUDIO"})}}).createMachine({id:"track",initial:"idle",context:r,on:{START_FETCH:{guard:"canStartFetch",actions:d({webAudioLoadingState:()=>"LOADING",fetchDecodeRef:({context:e,spawn:t})=>t("fetchDecode",{id:"fetchDecode",input:{trackUrl:e.trackUrl,resolvedUrl:e.resolvedUrl,skipHEAD:e.skipHEAD}})})}},states:{idle:{on:{HTML5_ENDED:{actions:["notifyTrackEnded"]},DEACTIVATE:{actions:["resetHtml5Element","resetTiming","stopProgressLoop","clearScheduleAndLookahead"]},ACTIVATE:{actions:["resetTiming","resetHtml5Element","clearScheduleAndLookahead"]},PLAY:[{guard:"canPlayWebAudio",target:"webaudio",actions:["setPlayingWebAudio","startSourceNode","startProgressLoop"]},{target:"html5",actions:["setIsPlaying","playHtml5","startProgressLoop"]}],PLAY_WEBAUDIO:{target:"webaudio",actions:["setPlayingWebAudio","startSourceNode","startProgressLoop"]},SCHEDULE_GAPLESS:{target:"webaudio",actions:["setScheduledGapless","startScheduledSourceNode"]},PRELOAD:{target:"loading"},BUFFER_LOADING:{actions:"setLoadingState"},BUFFER_READY:{actions:"setLoadedState"},BUFFER_ERROR:{actions:"setErrorState"},URL_RESOLVED:{actions:"setResolvedUrl"}}},html5:{on:{PAUSE:{actions:["clearIsPlaying","pauseHtml5","stopProgressLoop","reportProgress"]},PLAY:{actions:["setIsPlaying","playHtml5","startProgressLoop"]},BUFFER_LOADING:{actions:"setLoadingState"},PLAY_WEBAUDIO:{target:"webaudio",actions:"setPlayingWebAudio"},BUFFER_READY:{actions:"setLoadedState"},BUFFER_ERROR:{actions:"setErrorState"},SEEK:{actions:["seekHtml5","reportProgress"]},LOOKAHEAD_REACHED:{actions:"setNotifiedLookahead"},HTML5_ENDED:{target:"idle",actions:["clearIsPlaying","stopProgressLoop","notifyTrackEnded"]},ACTIVATE:{target:"idle",actions:["clearPlayingAndSchedule","pauseHtml5","stopProgressLoop","resetTiming","resetHtml5Element"]},URL_RESOLVED:{actions:"setResolvedUrl"},DEACTIVATE:{target:"idle",actions:["clearIsPlaying","pauseHtml5","resetHtml5Element","resetTiming","stopProgressLoop"]}}},loading:{on:{BUFFER_LOADING:{actions:"setLoadingState"},BUFFER_READY:{target:"idle",actions:"setLoadedState"},BUFFER_ERROR:{target:"idle",actions:"setErrorState"},PLAY:[{guard:"canPlayWebAudio",target:"webaudio",actions:["setPlayingWebAudio","startSourceNode","startProgressLoop"]},{target:"html5",actions:["setIsPlaying","playHtml5","startProgressLoop"]}],PLAY_WEBAUDIO:{target:"webaudio",actions:["setPlayingWebAudio","startSourceNode","startProgressLoop"]},SCHEDULE_GAPLESS:{target:"webaudio",actions:["setScheduledGapless","startScheduledSourceNode"]},ACTIVATE:{target:"idle",actions:["clearPlayingAndSchedule","resetTiming","resetHtml5Element"]},DEACTIVATE:{target:"idle",actions:["clearIsPlaying","resetTiming"]},URL_RESOLVED:{actions:"setResolvedUrl"}}},webaudio:{on:{PAUSE:{actions:["freezePausedTime","clearIsPlaying","stopSourceNode","disconnectGain","stopProgressLoop","reportProgress"]},PLAY:[{guard:"canPlayWebAudio",actions:["setIsPlaying","startSourceNode","startProgressLoop"]},{actions:"setIsPlaying"}],PLAY_WEBAUDIO:{actions:"setPlayingWebAudioType"},SEEK:{actions:["clearScheduledStart","seekWebAudio","reportProgress"]},SET_VOLUME:{},CANCEL_GAPLESS:{target:"idle",actions:["clearPlayingAndSchedule","stopSourceNode","disconnectGain","stopProgressLoop","resetTiming"]},LOOKAHEAD_REACHED:{actions:"setNotifiedLookahead"},WEBAUDIO_ENDED:{target:"idle",actions:["clearIsPlaying","stopProgressLoop","notifyTrackEnded"]},ACTIVATE:{target:"idle",actions:["clearPlayingAndSchedule","stopSourceNode","disconnectGain","stopProgressLoop","resetTiming","resetHtml5Element"]},DEACTIVATE:{target:"idle",actions:["clearPlayingAndSchedule","stopSourceNode","disconnectGain","resetTiming","resetHtml5Element","stopProgressLoop"]}}}}})}var K=5,g=class{index;metadata;_trackUrl;_resolvedUrl;skipHEAD;_pendingArrayBuffer=null;audio;_webAudioDisabled;get ctx(){if(this._webAudioDisabled)return null;let e=k();return e&&!this.gainNode&&(this.gainNode=e.createGain(),this.gainNode.gain.value=this.audio.volume),e}gainNode=null;sourceNode=null;audioBuffer=null;webAudioStartedAt=0;pausedAtTrackTime=0;_actor;queueRef;rafId=null;constructor(e){this.index=e.index,this._trackUrl=e.trackUrl,this._resolvedUrl=e.trackUrl,this.skipHEAD=e.skipHEAD??!1,this.metadata=e.metadata??{},this.queueRef=e.queue,this.audio=new Audio,this.audio.preload="none",this.audio.src=this._trackUrl,this.audio.volume=e.queue.volume,this.audio.controls=!1,this.audio.onerror=()=>{var u,l;let n=(u=this.audio.error)==null?void 0:u.code;if(n===1)return;let s=((l=this.audio.error)==null?void 0:l.message)??"unknown";this.queueRef.onError(new Error(`HTML5 audio error on track ${this.index} (code ${n}): ${s}`))},this.audio.onended=()=>{this.queueRef.onDebug(`audio.onended track=${this.index} machineState=${this._actor.getSnapshot().value} queueIdx=${this.queueRef.currentTrackIndex}`),this._actor.send({type:"HTML5_ENDED"})},this._webAudioDisabled=e.queue.webAudioIsDisabled;let t={trackUrl:this._trackUrl,resolvedUrl:this._trackUrl,skipHEAD:this.skipHEAD,playbackType:"HTML5",webAudioLoadingState:"NONE",isPlaying:!1,scheduledStartContextTime:null,notifiedLookahead:!1,fetchDecodeRef:null},i=N(t).provide({guards:{canPlayWebAudio:()=>!!(this.ctx&&this.audioBuffer&&this.gainNode)},actors:{fetchDecode:A.provide({actors:{resolveUrl:S(async({signal:n})=>{let s=await fetch(this._trackUrl,{method:"HEAD",signal:n});return s.redirected&&s.url?(this._resolvedUrl=s.url,this.audio.src=s.url,s.url):null}),fetchAudio:S(async({input:n,signal:s})=>{let{resolvedUrl:u}=n,l=await fetch(u,{signal:s});if(!l.ok)throw new Error(`HTTP ${l.status} for ${u}`);this._pendingArrayBuffer=await l.arrayBuffer()}),decodeAudio:S(async()=>{let n=this._pendingArrayBuffer;if(this._pendingArrayBuffer=null,!n||!this.ctx)throw new Error("No ArrayBuffer or AudioContext");this.audioBuffer=await this.ctx.decodeAudioData(n),queueMicrotask(()=>this.queueRef.onTrackBufferReady(this))})}})},actions:{playHtml5:()=>this._playHtml5(),startSourceNode:()=>{this._startSourceNode(this.pausedAtTrackTime)},startScheduledSourceNode:({context:n})=>{let s=n.scheduledStartContextTime;s===null||!this.ctx||!this.audioBuffer||!this.gainNode||(this._stopSourceNode(),this.sourceNode=this.ctx.createBufferSource(),this.sourceNode.buffer=this.audioBuffer,this.sourceNode.connect(this.gainNode),this.gainNode.connect(this.ctx.destination),this.sourceNode.onended=this._handleWebAudioEnded,this.sourceNode.start(s,0),this.webAudioStartedAt=s,this.queueRef.onDebug(`startScheduledSourceNode track=${this.index} when=${s.toFixed(3)} ctxNow=${this.ctx.currentTime.toFixed(3)} delta=${(s-this.ctx.currentTime).toFixed(3)}s`))},startProgressLoop:()=>this.startProgressLoop(),pauseHtml5:()=>this.audio.pause(),freezePausedTime:()=>{this.pausedAtTrackTime=this.currentTime},stopSourceNode:()=>this._stopSourceNode(),disconnectGain:()=>this._disconnectGain(),stopProgressLoop:()=>this._stopProgressLoop(),reportProgress:()=>this.queueRef.onProgress(this.toInfo()),seekHtml5:()=>this._seekHtml5(),seekWebAudio:()=>this._seekWebAudio(),resetHtml5Element:()=>{this.audio.currentTime=0},resetTiming:()=>{this.webAudioStartedAt=0,this.pausedAtTrackTime=0},notifyTrackEnded:()=>{queueMicrotask(()=>this.queueRef.onTrackEnded(this))}}});this._actor=V(i),this._actor.start()}play(){this.queueRef.onDebug(`Track.play() track=${this.index} machineState=${this._actor.getSnapshot().value} hasBuffer=${!!this.audioBuffer} hasCtx=${!!this.ctx} audioPaused=${this.audio.paused}`),this._actor.send({type:"PLAY"})}pause(){this._actor.send({type:"PAUSE"})}seek(e){let t=Math.max(0,isNaN(this.duration)?e:Math.min(e,this.duration));this.pausedAtTrackTime=t,this._actor.send({type:"SEEK",time:t})}setVolume(e){let t=Math.min(1,Math.max(0,e));this.audio.volume=t,this.gainNode&&(this.gainNode.gain.value=t),this._actor.send({type:"SET_VOLUME",volume:t})}preload(){this.queueRef.onDebug(`preload() track=${this.index} state=${this._actor.getSnapshot().value} hasBuffer=${!!this.audioBuffer} hasCtx=${!!this.ctx}`),this._actor.getSnapshot().value==="idle"&&this._actor.send({type:"PRELOAD"}),!(this.audioBuffer||!this.ctx)&&this._actor.send({type:"START_FETCH"})}seekToEnd(e=6){let t=this.duration;!isNaN(t)&&t>e&&this.seek(t-e)}activate(){this._actor.send({type:"ACTIVATE"})}deactivate(){this.queueRef.onDebug(`Track.deactivate() track=${this.index} machineState=${this._actor.getSnapshot().value} isPlaying=${this.isPlaying}`),this._actor.send({type:"DEACTIVATE"}),this.queueRef.onDebug(`Track.deactivate() done track=${this.index} machineState=${this._actor.getSnapshot().value}`)}destroy(){var e;this.deactivate(),this._pendingArrayBuffer=null,this.audioBuffer=null,(e=this.gainNode)==null||e.disconnect(),this.gainNode=null,this._actor.stop()}cancelGaplessStart(){this._actor.getSnapshot().context.scheduledStartContextTime!==null&&this._actor.send({type:"CANCEL_GAPLESS"})}scheduleGaplessStart(e){!this.ctx||!this.audioBuffer||!this.gainNode||this._actor.send({type:"SCHEDULE_GAPLESS",when:e})}get currentTime(){let e=this._actor.getSnapshot();return e.value==="webaudio"?e.context.isPlaying?this.ctx?Math.max(0,this.ctx.currentTime-this.webAudioStartedAt):0:this.pausedAtTrackTime:this.audio.currentTime}get duration(){return this.audioBuffer?this.audioBuffer.duration:this.audio.duration}get isPaused(){let e=this._actor.getSnapshot();return e.value==="webaudio"?!e.context.isPlaying:this.audio.paused}get isPlaying(){return this._actor.getSnapshot().context.isPlaying}get trackUrl(){return this._resolvedUrl}get playbackType(){return this._actor.getSnapshot().context.playbackType}get webAudioLoadingState(){return this._actor.getSnapshot().context.webAudioLoadingState}get hasSourceNode(){return this.sourceNode!==null}get machineState(){return this._actor.getSnapshot().value}get scheduledStartContextTime(){return this._actor.getSnapshot().context.scheduledStartContextTime}get isBufferLoaded(){return this.audioBuffer!==null}toInfo(){var e;return{index:this.index,currentTime:this.currentTime,duration:this.duration,isPlaying:this.isPlaying,isPaused:this.isPaused,volume:((e=this.gainNode)==null?void 0:e.gain.value)??this.audio.volume,trackUrl:this.trackUrl,playbackType:this.playbackType,webAudioLoadingState:this.webAudioLoadingState,metadata:this.metadata,machineState:this.machineState}}_playHtml5(){this.audio.preload!=="auto"&&(this.audio.preload="auto");let e=this.audio.play();e&&e.catch(t=>{t instanceof Error&&t.name==="NotAllowedError"?this.queueRef.onPlayBlocked():t instanceof Error&&t.name==="AbortError"||this.queueRef.onError(t instanceof Error?t:new Error(String(t)))})}_seekHtml5(){let e=this.pausedAtTrackTime;this.audio.preload!=="auto"&&(this.audio.preload="auto"),this.audio.readyState>=HTMLMediaElement.HAVE_METADATA?this.audio.currentTime=e:(this.audio.addEventListener("loadedmetadata",()=>{this.audio.currentTime=e},{once:!0}),this.audio.load())}_startSourceNode(e){!this.ctx||!this.audioBuffer||!this.gainNode||(this._stopSourceNode(),this.ctx.state==="suspended"&&this.ctx.resume(),this.sourceNode=this.ctx.createBufferSource(),this.sourceNode.buffer=this.audioBuffer,this.sourceNode.connect(this.gainNode),this.gainNode.connect(this.ctx.destination),this.sourceNode.onended=this._handleWebAudioEnded,this.webAudioStartedAt=this.ctx.currentTime-e,this.sourceNode.start(0,e))}_stopSourceNode(){if(this.sourceNode){this.sourceNode.onended=null;try{this.sourceNode.stop()}catch{}try{this.sourceNode.disconnect()}catch{}this.sourceNode=null}}_disconnectGain(){if(!(!this.gainNode||!this.ctx))try{this.gainNode.disconnect(this.ctx.destination)}catch{}}_seekWebAudio(){let t=this._actor.getSnapshot().context.isPlaying,i=this.pausedAtTrackTime;this._stopSourceNode(),t&&this._startSourceNode(i)}_handleWebAudioEnded=()=>{this.queueRef.onDebug(`_handleWebAudioEnded track=${this.index} sourceNode=${!!this.sourceNode} queueIdx=${this.queueRef.currentTrackIndex}`),this.sourceNode&&this._actor.send({type:"WEBAUDIO_ENDED"})};startProgressLoop(){if(this.rafId!==null)return;let e=()=>{if(this.isPaused||!this.isPlaying){this.rafId=null;return}this.queueRef.onProgress(this.toInfo());let t=this.duration-this.currentTime;!this._actor.getSnapshot().context.notifiedLookahead&&!isNaN(t)&&t<=K&&(this._actor.send({type:"LOOKAHEAD_REACHED"}),queueMicrotask(()=>this.queueRef.onTrackBufferReady(this))),this.rafId=requestAnimationFrame(e)};this.rafId=requestAnimationFrame(e)}_stopProgressLoop(){this.rafId!==null&&(cancelAnimationFrame(this.rafId),this.rafId=null)}};function L(r,e){let t=0;return(...i)=>{let n=performance.now();n-t<e||(t=n,r(...i))}}var Q=2,f=class{_tracks=[];_actor;_onProgress;_onEnded;_onPlayNextTrack;_onPlayPreviousTrack;_onStartNewTrack;_onError;_onPlayBlocked;_onQueueStateChange;_onDebug;webAudioIsDisabled;_volume;_scheduledNextIndex=null;_throttledUpdatePositionState=L((e,t)=>m(e,t),1e3);constructor(e={}){let{tracks:t=[],onProgress:i,onEnded:n,onPlayNextTrack:s,onPlayPreviousTrack:u,onStartNewTrack:l,onError:I,onPlayBlocked:R,onQueueStateChange:U,onDebug:M,webAudioIsDisabled:w=!1,trackMetadata:O=[],volume:G=1}=e;this._volume=Math.min(1,Math.max(0,G)),this.webAudioIsDisabled=w,this._onProgress=i,this._onEnded=n,this._onPlayNextTrack=s,this._onPlayPreviousTrack=u,this._onStartNewTrack=l,this._onError=I,this._onPlayBlocked=R,this._onQueueStateChange=U,this._onDebug=M,this._tracks=t.map((o,a)=>new g({trackUrl:o,index:a,queue:this,metadata:O[a]}));let H=D({currentTrackIndex:0,trackCount:this._tracks.length}).provide({actions:{deactivateCurrent:({context:o})=>{var a;(a=this._trackAt(o.currentTrackIndex))==null||a.deactivate()},deactivateEndedTrack:({context:o})=>{var a;(a=this._trackAt(o.currentTrackIndex))==null||a.deactivate()},activateAndPlayCurrent:({context:o})=>{let a=this._trackAt(o.currentTrackIndex);a&&(a.activate(),this._scheduledNextIndex!==a.index&&a.play())},playOrContinueGapless:({context:o})=>{let a=this._trackAt(o.currentTrackIndex);a&&(this._scheduledNextIndex!==a.index?a.play():(this._scheduledNextIndex=null,this.onDebug(`onTrackEnded: gapless track ${a.index} \u2014 sourceNode=${a.hasSourceNode} isPlaying=${a.isPlaying} machineState=${a.machineState}`),a.startProgressLoop()))},cancelAllGapless:()=>this._cancelScheduledGapless(),notifyStartNewTrack:({context:o})=>{var c;let a=this._trackAt(o.currentTrackIndex);a&&((c=this._onStartNewTrack)==null||c.call(this,a.toInfo()))},notifyPlayNextTrack:({context:o})=>{var c;let a=this._trackAt(o.currentTrackIndex);a&&((c=this._onPlayNextTrack)==null||c.call(this,a.toInfo()))},notifyPlayPreviousTrack:({context:o})=>{var c;let a=this._trackAt(o.currentTrackIndex);a&&((c=this._onPlayPreviousTrack)==null||c.call(this,a.toInfo()))},notifyEnded:()=>{var o;return(o=this._onEnded)==null?void 0:o.call(this)},updateMediaSessionMetadata:({context:o})=>{let a=this._trackAt(o.currentTrackIndex);a&&P(a.metadata)},preloadAhead:({context:o})=>{this._preloadAhead(o.currentTrackIndex)},playCurrent:({context:o})=>{var a;(a=this._trackAt(o.currentTrackIndex))==null||a.play()},pauseCurrent:({context:o})=>{var a;(a=this._trackAt(o.currentTrackIndex))==null||a.pause()},seekCurrent:({context:o,event:a})=>{var _;let c=a;(_=this._trackAt(o.currentTrackIndex))==null||_.seek(c.time)},seekCurrentToZero:({context:o})=>{var a;(a=this._trackAt(o.currentTrackIndex))==null||a.seek(0)},scheduleGapless:({context:o})=>{this._tryScheduleGapless(o.currentTrackIndex)},cancelScheduledGapless:()=>{this._cancelScheduledGapless()},cancelAndRescheduleGapless:({context:o})=>{this._cancelScheduledGapless(),this._tryScheduleGapless(o.currentTrackIndex)}}});this._actor=Y(H),this._actor.subscribe(o=>{var c;b(o.value==="playing");let a=this._trackAt(o.context.currentTrackIndex);a&&!isNaN(a.duration)&&m(a.duration,a.currentTime),(c=this._onQueueStateChange)==null||c.call(this,{state:o.value,context:o.context})}),this._actor.start(),v({onPlay:()=>this.play(),onPause:()=>{this._actor.getSnapshot().value==="playing"&&this.pause()},onNext:()=>this.next(),onPrevious:()=>this.previous(),onSeek:o=>this.seek(o)})}play(){this._currentTrack&&this._actor.send({type:"PLAY"})}pause(){this._actor.send({type:"PAUSE"})}togglePlayPause(){this._actor.getSnapshot().value==="playing"?this.pause():this.play()}next(){this._actor.getSnapshot().context.currentTrackIndex+1>=this._tracks.length||this._actor.send({type:"NEXT"})}previous(){let e=this._currentTrack;if(e&&e.currentTime>8){e.seek(0),e.play();return}this._actor.send({type:"PREVIOUS"})}gotoTrack(e,t=!1){e<0||e>=this._tracks.length||(this.onDebug(`gotoTrack(${e}, playImmediately=${t}) queueState=${this._actor.getSnapshot().value} curIdx=${this._actor.getSnapshot().context.currentTrackIndex}`),this._actor.send({type:"GOTO",index:e,playImmediately:t}))}seek(e){this._actor.send({type:"SEEK",time:e})}setVolume(e){let t=Math.min(1,Math.max(0,e));this._volume=t;for(let i of this._tracks)i.setVolume(t)}addTrack(e,t={}){let i=this._tracks.length,n=t.metadata??{};this._tracks.push(new g({trackUrl:e,index:i,queue:this,skipHEAD:t.skipHEAD,metadata:n})),this._actor.send({type:"ADD_TRACK"})}removeTrack(e){if(!(e<0||e>=this._tracks.length)){this._tracks[e].destroy(),this._tracks.splice(e,1);for(let t=e;t<this._tracks.length;t++)this._tracks[t].index=t;this._scheduledNextIndex===e&&(this._scheduledNextIndex=null),this._actor.send({type:"REMOVE_TRACK",index:e})}}resumeAudioContext(){return x()}destroy(){for(let e of this._tracks)e.destroy();this._tracks=[],this._actor.stop()}get currentTrack(){var e;return(e=this._currentTrack)==null?void 0:e.toInfo()}get currentTrackIndex(){return this._actor.getSnapshot().context.currentTrackIndex}get tracks(){return this._tracks.map(e=>e.toInfo())}get isPlaying(){return this._actor.getSnapshot().value==="playing"}get isPaused(){return this._actor.getSnapshot().value==="paused"}get volume(){return this._volume}get queueSnapshot(){let e=this._actor.getSnapshot();return{state:e.value,context:e.context}}onTrackEnded(e){let t=this._actor.getSnapshot();if(this.onDebug(`onTrackEnded track=${e.index} queueState=${t.value} curIdx=${t.context.currentTrackIndex}`),e.index!==t.context.currentTrackIndex)return;this._actor.send({type:"TRACK_ENDED"});let i=this._actor.getSnapshot();this.onDebug(`onTrackEnded after TRACK_ENDED \u2192 queueState=${i.value} curIdx=${i.context.currentTrackIndex}`)}onTrackBufferReady(e){this._actor.send({type:"TRACK_LOADED",index:e.index})}onProgress(e){var t;e.index===this._actor.getSnapshot().context.currentTrackIndex&&(isNaN(e.duration)||this._throttledUpdatePositionState(e.duration,e.currentTime),(t=this._onProgress)==null||t.call(this,e))}onError(e){var t;(t=this._onError)==null||t.call(this,e)}onPlayBlocked(){var e;(e=this._onPlayBlocked)==null||e.call(this)}onDebug(e){var t;(t=this._onDebug)==null||t.call(this,e)}_trackAt(e){return this._tracks[e]}get _currentTrack(){return this._tracks[this._actor.getSnapshot().context.currentTrackIndex]}_preloadAhead(e){let t=e+Q+1;this.onDebug(`_preloadAhead(${e}) limit=${t} trackCount=${this._tracks.length}`);for(let i=e+1;i<this._tracks.length&&i<t;i++){let n=this._tracks[i];if(n.isBufferLoaded)this.onDebug(`_preloadAhead: track ${i} already loaded`);else{this.onDebug(`_preloadAhead: starting preload for track ${i}`),n.preload();break}}}_cancelScheduledGapless(){if(this._scheduledNextIndex===null)return;let e=this._trackAt(this._scheduledNextIndex);e&&(e.cancelGaplessStart(),this.onDebug(`_cancelScheduledGapless: cancelled track ${this._scheduledNextIndex}`)),this._scheduledNextIndex=null}_tryScheduleGapless(e){let t=k();if(!t||this.webAudioIsDisabled)return;let i=e+1;if(i>=this._tracks.length)return;let n=this._tracks[e],s=this._tracks[i];if(!n.isBufferLoaded||!s.isBufferLoaded||this._scheduledNextIndex===i||!n.isPlaying)return;let u=this._computeTrackEndTime(n);u!==null&&(u<t.currentTime+.01||(s.scheduleGaplessStart(u),this._scheduledNextIndex=i))}_computeTrackEndTime(e){let t=k();if(!t||!e.isBufferLoaded)return null;let i=e.duration;if(isNaN(i))return null;if(e.scheduledStartContextTime!==null)return e.scheduledStartContextTime+i;let n=i-e.currentTime;return n<=0?null:t.currentTime+n}};export{f as Queue,f as default};
|
|
2
|
+
//# sourceMappingURL=index.mjs.map
|