gapless.js 2.2.3 → 4.0.0

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.
@@ -0,0 +1,218 @@
1
+ // ---------------------------------------------------------------------------
2
+ // QueueMachine — top-level queue state (xstate v5)
3
+ //
4
+ // States:
5
+ // idle No tracks, or not yet started.
6
+ // playing A track is actively playing.
7
+ // paused Explicitly paused by the user.
8
+ // ended The last track in the queue has finished.
9
+ //
10
+ // Root-level `on:` eliminates the handler duplication that plagued v2.
11
+ // ---------------------------------------------------------------------------
12
+
13
+ import { setup, assign } from 'xstate';
14
+
15
+ // ---- Context ---------------------------------------------------------------
16
+
17
+ export interface QueueContext {
18
+ currentTrackIndex: number;
19
+ trackCount: number;
20
+ }
21
+
22
+ // ---- Events ----------------------------------------------------------------
23
+
24
+ export type QueueEvent =
25
+ | { type: 'PLAY' }
26
+ | { type: 'PAUSE' }
27
+ | { type: 'TOGGLE' }
28
+ | { type: 'NEXT' }
29
+ | { type: 'PREVIOUS' }
30
+ | { type: 'GOTO'; index: number; playImmediately?: boolean }
31
+ | { type: 'SEEK'; time: number }
32
+ | { type: 'SET_VOLUME'; volume: number }
33
+ | { type: 'ADD_TRACK' }
34
+ | { type: 'REMOVE_TRACK'; index: number }
35
+ | { type: 'TRACK_ENDED' }
36
+ | { type: 'TRACK_LOADED'; index: number };
37
+
38
+ // ---- Machine ---------------------------------------------------------------
39
+
40
+ export function createQueueMachine(initialContext: QueueContext) {
41
+ return setup({
42
+ types: {
43
+ context: {} as QueueContext,
44
+ events: {} as QueueEvent,
45
+ },
46
+ guards: {
47
+ hasNextTrack: ({ context }) => context.currentTrackIndex + 1 < context.trackCount,
48
+ playImmediately: ({ event }) =>
49
+ !!(event as { type: 'GOTO'; playImmediately?: boolean }).playImmediately,
50
+ },
51
+ }).createMachine({
52
+ id: 'queue',
53
+ initial: 'idle',
54
+ context: initialContext,
55
+
56
+ // Global handlers — shared across all states
57
+ on: {
58
+ ADD_TRACK: {
59
+ actions: assign({ trackCount: ({ context }) => context.trackCount + 1 }),
60
+ },
61
+ REMOVE_TRACK: {
62
+ actions: assign({
63
+ trackCount: ({ context }) => Math.max(0, context.trackCount - 1),
64
+ currentTrackIndex: ({ context, event }) => {
65
+ const e = event as { type: 'REMOVE_TRACK'; index: number };
66
+ if (e.index < context.currentTrackIndex) {
67
+ return Math.max(0, context.currentTrackIndex - 1);
68
+ }
69
+ return context.currentTrackIndex;
70
+ },
71
+ }),
72
+ },
73
+ },
74
+
75
+ states: {
76
+ // -----------------------------------------------------------------
77
+ // idle
78
+ // -----------------------------------------------------------------
79
+ idle: {
80
+ on: {
81
+ PLAY: { target: 'playing' },
82
+ GOTO: [
83
+ {
84
+ guard: 'playImmediately',
85
+ target: 'playing',
86
+ actions: assign({
87
+ currentTrackIndex: ({ event }) => (event as { type: 'GOTO'; index: number }).index,
88
+ }),
89
+ },
90
+ {
91
+ target: 'paused',
92
+ actions: assign({
93
+ currentTrackIndex: ({ event }) => (event as { type: 'GOTO'; index: number }).index,
94
+ }),
95
+ },
96
+ ],
97
+ },
98
+ },
99
+
100
+ // -----------------------------------------------------------------
101
+ // playing
102
+ // -----------------------------------------------------------------
103
+ playing: {
104
+ on: {
105
+ PAUSE: { target: 'paused' },
106
+ TOGGLE: { target: 'paused' },
107
+ NEXT: {
108
+ actions: assign({
109
+ currentTrackIndex: ({ context }) => {
110
+ const next = context.currentTrackIndex + 1;
111
+ return next < context.trackCount ? next : context.currentTrackIndex;
112
+ },
113
+ }),
114
+ },
115
+ PREVIOUS: {
116
+ actions: assign({
117
+ currentTrackIndex: ({ context }) => Math.max(0, context.currentTrackIndex - 1),
118
+ }),
119
+ },
120
+ GOTO: {
121
+ actions: assign({
122
+ currentTrackIndex: ({ event }) => (event as { type: 'GOTO'; index: number }).index,
123
+ }),
124
+ },
125
+ SEEK: {},
126
+ TRACK_ENDED: [
127
+ {
128
+ guard: 'hasNextTrack',
129
+ target: 'playing',
130
+ actions: assign({
131
+ currentTrackIndex: ({ context }) => context.currentTrackIndex + 1,
132
+ }),
133
+ },
134
+ { target: 'ended' },
135
+ ],
136
+ TRACK_LOADED: {},
137
+ },
138
+ },
139
+
140
+ // -----------------------------------------------------------------
141
+ // paused
142
+ // -----------------------------------------------------------------
143
+ paused: {
144
+ on: {
145
+ PLAY: { target: 'playing' },
146
+ TOGGLE: { target: 'playing' },
147
+ NEXT: {
148
+ actions: assign({
149
+ currentTrackIndex: ({ context }) => {
150
+ const next = context.currentTrackIndex + 1;
151
+ return next < context.trackCount ? next : context.currentTrackIndex;
152
+ },
153
+ }),
154
+ },
155
+ PREVIOUS: {
156
+ actions: assign({
157
+ currentTrackIndex: ({ context }) => Math.max(0, context.currentTrackIndex - 1),
158
+ }),
159
+ },
160
+ GOTO: [
161
+ {
162
+ guard: 'playImmediately',
163
+ target: 'playing',
164
+ actions: assign({
165
+ currentTrackIndex: ({ event }) => (event as { type: 'GOTO'; index: number }).index,
166
+ }),
167
+ },
168
+ {
169
+ actions: assign({
170
+ currentTrackIndex: ({ event }) => (event as { type: 'GOTO'; index: number }).index,
171
+ }),
172
+ },
173
+ ],
174
+ SEEK: {},
175
+ TRACK_ENDED: [
176
+ {
177
+ guard: 'hasNextTrack',
178
+ target: 'paused',
179
+ actions: assign({
180
+ currentTrackIndex: ({ context }) => context.currentTrackIndex + 1,
181
+ }),
182
+ },
183
+ { target: 'ended' },
184
+ ],
185
+ },
186
+ },
187
+
188
+ // -----------------------------------------------------------------
189
+ // ended
190
+ // -----------------------------------------------------------------
191
+ ended: {
192
+ on: {
193
+ PLAY: {
194
+ target: 'playing',
195
+ actions: assign({ currentTrackIndex: () => 0 }),
196
+ },
197
+ GOTO: [
198
+ {
199
+ guard: 'playImmediately',
200
+ target: 'playing',
201
+ actions: assign({
202
+ currentTrackIndex: ({ event }) => (event as { type: 'GOTO'; index: number }).index,
203
+ }),
204
+ },
205
+ {
206
+ target: 'paused',
207
+ actions: assign({
208
+ currentTrackIndex: ({ event }) => (event as { type: 'GOTO'; index: number }).index,
209
+ }),
210
+ },
211
+ ],
212
+ },
213
+ },
214
+ },
215
+ });
216
+ }
217
+
218
+ export type QueueMachine = ReturnType<typeof createQueueMachine>;
@@ -0,0 +1,227 @@
1
+ // ---------------------------------------------------------------------------
2
+ // TrackMachine — per-track audio state (xstate v5)
3
+ //
4
+ // States:
5
+ // idle Initial state. Audio nodes not yet initialised.
6
+ // html5 HTML5 Audio is playing. Web Audio fetch+decode may be in progress.
7
+ // loading Track is preloaded (not yet playing). Decode in progress.
8
+ // webaudio AudioBufferSourceNode is the active output.
9
+ //
10
+ // Bug fixes in this rewrite:
11
+ // #2: BUFFER_READY in html5 stays in html5 (no longer auto-transitions to webaudio)
12
+ // #3: DEACTIVATE from webaudio → idle (was staying in webaudio)
13
+ // #4: Removed dead error state (webAudioLoadingState: 'ERROR' is sufficient)
14
+ // ---------------------------------------------------------------------------
15
+
16
+ import { setup, assign } from 'xstate';
17
+ import type { WebAudioLoadingState, PlaybackType } from '../types';
18
+
19
+ // ---- Context ---------------------------------------------------------------
20
+
21
+ export interface TrackContext {
22
+ trackUrl: string;
23
+ resolvedUrl: string;
24
+ skipHEAD: boolean;
25
+ playbackType: PlaybackType;
26
+ webAudioLoadingState: WebAudioLoadingState;
27
+ webAudioStartedAt: number;
28
+ pausedAtTrackTime: number;
29
+ isPlaying: boolean;
30
+ }
31
+
32
+ // ---- Events ----------------------------------------------------------------
33
+
34
+ export type TrackEvent =
35
+ | { type: 'ACTIVATE' }
36
+ | { type: 'DEACTIVATE' }
37
+ | { type: 'PRELOAD' }
38
+ | { type: 'BUFFER_LOADING' }
39
+ | { type: 'PLAY' }
40
+ | { type: 'PLAY_WEBAUDIO' }
41
+ | { type: 'PAUSE' }
42
+ | { type: 'SEEK'; time: number }
43
+ | { type: 'SET_VOLUME'; volume: number }
44
+ | { type: 'BUFFER_READY' }
45
+ | { type: 'BUFFER_ERROR' }
46
+ | { type: 'HTML5_ENDED' }
47
+ | { type: 'WEBAUDIO_ENDED' }
48
+ | { type: 'URL_RESOLVED'; url: string };
49
+
50
+ // ---- Machine ---------------------------------------------------------------
51
+
52
+ export function createTrackMachine(initialContext: TrackContext) {
53
+ return setup({
54
+ types: {
55
+ context: {} as TrackContext,
56
+ events: {} as TrackEvent,
57
+ },
58
+ }).createMachine({
59
+ id: 'track',
60
+ initial: 'idle',
61
+ context: initialContext,
62
+
63
+ states: {
64
+ // -----------------------------------------------------------------
65
+ // idle: constructed but not started
66
+ // -----------------------------------------------------------------
67
+ idle: {
68
+ on: {
69
+ HTML5_ENDED: {},
70
+ PLAY: {
71
+ target: 'html5',
72
+ actions: assign({ isPlaying: () => true }),
73
+ },
74
+ PLAY_WEBAUDIO: {
75
+ target: 'webaudio',
76
+ actions: assign({
77
+ isPlaying: () => true,
78
+ webAudioLoadingState: () => 'LOADED' as WebAudioLoadingState,
79
+ playbackType: () => 'WEBAUDIO' as PlaybackType,
80
+ }),
81
+ },
82
+ PRELOAD: { target: 'loading' },
83
+ BUFFER_LOADING: {
84
+ actions: assign({ webAudioLoadingState: () => 'LOADING' as WebAudioLoadingState }),
85
+ },
86
+ URL_RESOLVED: {
87
+ actions: assign({
88
+ resolvedUrl: ({ event }) => (event as { type: 'URL_RESOLVED'; url: string }).url,
89
+ }),
90
+ },
91
+ },
92
+ },
93
+
94
+ // -----------------------------------------------------------------
95
+ // html5: HTML5 Audio is playing; WebAudio decode may be in progress
96
+ // -----------------------------------------------------------------
97
+ html5: {
98
+ on: {
99
+ PAUSE: {
100
+ actions: assign({ isPlaying: () => false }),
101
+ },
102
+ PLAY: {
103
+ actions: assign({ isPlaying: () => true }),
104
+ },
105
+ BUFFER_LOADING: {
106
+ actions: assign({ webAudioLoadingState: () => 'LOADING' as WebAudioLoadingState }),
107
+ },
108
+ PLAY_WEBAUDIO: {
109
+ target: 'webaudio',
110
+ actions: assign({
111
+ isPlaying: () => true,
112
+ webAudioLoadingState: () => 'LOADED' as WebAudioLoadingState,
113
+ playbackType: () => 'WEBAUDIO' as PlaybackType,
114
+ }),
115
+ },
116
+ // Bug #2 fix: BUFFER_READY in html5 stays in html5, only updates loading state.
117
+ // The actual switchover to webaudio only happens via explicit PLAY_WEBAUDIO.
118
+ BUFFER_READY: {
119
+ actions: assign({
120
+ webAudioLoadingState: () => 'LOADED' as WebAudioLoadingState,
121
+ }),
122
+ },
123
+ BUFFER_ERROR: {
124
+ actions: assign({
125
+ webAudioLoadingState: () => 'ERROR' as WebAudioLoadingState,
126
+ }),
127
+ },
128
+ SEEK: {
129
+ actions: assign({
130
+ pausedAtTrackTime: ({ event }) => (event as { type: 'SEEK'; time: number }).time,
131
+ }),
132
+ },
133
+ HTML5_ENDED: {
134
+ target: 'idle',
135
+ actions: assign({ isPlaying: () => false }),
136
+ },
137
+ URL_RESOLVED: {
138
+ actions: assign({
139
+ resolvedUrl: ({ event }) => (event as { type: 'URL_RESOLVED'; url: string }).url,
140
+ }),
141
+ },
142
+ DEACTIVATE: {
143
+ target: 'loading',
144
+ actions: assign({ isPlaying: () => false }),
145
+ },
146
+ },
147
+ },
148
+
149
+ // -----------------------------------------------------------------
150
+ // loading: preloading in background (not the active track yet)
151
+ // -----------------------------------------------------------------
152
+ loading: {
153
+ on: {
154
+ BUFFER_LOADING: {
155
+ actions: assign({ webAudioLoadingState: () => 'LOADING' as WebAudioLoadingState }),
156
+ },
157
+ BUFFER_READY: {
158
+ target: 'idle',
159
+ actions: assign({
160
+ webAudioLoadingState: () => 'LOADED' as WebAudioLoadingState,
161
+ }),
162
+ },
163
+ BUFFER_ERROR: {
164
+ target: 'idle',
165
+ actions: assign({
166
+ webAudioLoadingState: () => 'ERROR' as WebAudioLoadingState,
167
+ }),
168
+ },
169
+ PLAY: {
170
+ target: 'html5',
171
+ actions: assign({ isPlaying: () => true }),
172
+ },
173
+ PLAY_WEBAUDIO: {
174
+ target: 'webaudio',
175
+ actions: assign({
176
+ isPlaying: () => true,
177
+ webAudioLoadingState: () => 'LOADED' as WebAudioLoadingState,
178
+ playbackType: () => 'WEBAUDIO' as PlaybackType,
179
+ }),
180
+ },
181
+ URL_RESOLVED: {
182
+ actions: assign({
183
+ resolvedUrl: ({ event }) => (event as { type: 'URL_RESOLVED'; url: string }).url,
184
+ }),
185
+ },
186
+ },
187
+ },
188
+
189
+ // -----------------------------------------------------------------
190
+ // webaudio: AudioBufferSourceNode is driving output
191
+ // -----------------------------------------------------------------
192
+ webaudio: {
193
+ on: {
194
+ PAUSE: {
195
+ actions: assign({ isPlaying: () => false }),
196
+ },
197
+ PLAY: {
198
+ actions: assign({ isPlaying: () => true }),
199
+ },
200
+ PLAY_WEBAUDIO: {
201
+ actions: assign({
202
+ isPlaying: () => true,
203
+ playbackType: () => 'WEBAUDIO' as PlaybackType,
204
+ }),
205
+ },
206
+ SEEK: {
207
+ actions: assign({
208
+ pausedAtTrackTime: ({ event }) => (event as { type: 'SEEK'; time: number }).time,
209
+ }),
210
+ },
211
+ SET_VOLUME: {},
212
+ WEBAUDIO_ENDED: {
213
+ target: 'idle',
214
+ actions: assign({ isPlaying: () => false }),
215
+ },
216
+ // Bug #3 fix: DEACTIVATE from webaudio → idle (was staying in webaudio)
217
+ DEACTIVATE: {
218
+ target: 'idle',
219
+ actions: assign({ isPlaying: () => false }),
220
+ },
221
+ },
222
+ },
223
+ },
224
+ });
225
+ }
226
+
227
+ export type TrackMachine = ReturnType<typeof createTrackMachine>;
package/src/types.ts ADDED
@@ -0,0 +1,87 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Public types for gapless.js
3
+ // ---------------------------------------------------------------------------
4
+
5
+ export type PlaybackType = 'HTML5' | 'WEBAUDIO';
6
+
7
+ export type WebAudioLoadingState = 'NONE' | 'LOADING' | 'LOADED' | 'ERROR';
8
+
9
+ /** Metadata attached to a track (arbitrary user data). */
10
+ export interface TrackMetadata {
11
+ title?: string;
12
+ artist?: string;
13
+ album?: string;
14
+ artwork?: MediaImage[];
15
+ [key: string]: unknown;
16
+ }
17
+
18
+ /** Options accepted by the Queue constructor. */
19
+ export interface GaplessOptions {
20
+ /** Initial list of track URLs. */
21
+ tracks?: string[];
22
+ /** Called at ~60fps while playing. */
23
+ onProgress?: (info: TrackInfo) => void;
24
+ /** Called when the last track in the queue ends. */
25
+ onEnded?: () => void;
26
+ /** Called when the queue advances to the next track. */
27
+ onPlayNextTrack?: (info: TrackInfo) => void;
28
+ /** Called when the queue goes back to the previous track. */
29
+ onPlayPreviousTrack?: (info: TrackInfo) => void;
30
+ /** Called whenever a new track becomes the current track. */
31
+ onStartNewTrack?: (info: TrackInfo) => void;
32
+ /** Called on HTML5 audio errors. */
33
+ onError?: (error: Error) => void;
34
+ /** Called with internal debug messages. Only use for development. */
35
+ onDebug?: (msg: string) => void;
36
+ /** Called when autoplay is blocked by the browser. */
37
+ onPlayBlocked?: () => void;
38
+ /**
39
+ * Set true to disable Web Audio API entirely and use HTML5 audio only.
40
+ * Gapless playback will not be available in this mode.
41
+ */
42
+ webAudioIsDisabled?: boolean;
43
+ /** Per-track metadata (aligned to the tracks array by index). */
44
+ trackMetadata?: TrackMetadata[];
45
+ /** Initial volume, 0.0–1.0. Defaults to 1. */
46
+ volume?: number;
47
+ }
48
+
49
+ /** Options for dynamically adding a track. */
50
+ export 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
+ /**
60
+ * A plain-data snapshot of a track's current state.
61
+ * Returned by Queue getters and passed to callbacks.
62
+ * No methods — pure data.
63
+ */
64
+ export interface TrackInfo {
65
+ /** Zero-based position of this track in the queue. */
66
+ index: number;
67
+ /** Current playback position in seconds. */
68
+ currentTime: number;
69
+ /** Total duration in seconds (NaN until loaded). */
70
+ duration: number;
71
+ /** True if currently playing. */
72
+ isPlaying: boolean;
73
+ /** True if explicitly paused. */
74
+ isPaused: boolean;
75
+ /** Current volume, 0.0–1.0. */
76
+ volume: number;
77
+ /** The resolved URL of the audio file. */
78
+ trackUrl: string;
79
+ /** Which backend is currently producing sound. */
80
+ playbackType: PlaybackType;
81
+ /** Whether the Web Audio buffer has been decoded. */
82
+ webAudioLoadingState: WebAudioLoadingState;
83
+ /** Arbitrary metadata supplied when the track was added. */
84
+ metadata?: TrackMetadata;
85
+ /** Current xstate machine state for this track (e.g. 'idle', 'html5', 'webaudio'). */
86
+ machineState: string;
87
+ }
@@ -0,0 +1,69 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Singleton AudioContext manager
3
+ //
4
+ // A single AudioContext is shared across all Track instances. This is
5
+ // essential: all AudioBufferSourceNodes must share the same context so that
6
+ // audioContext.currentTime is a common monotonic clock, enabling
7
+ // sample-accurate scheduling of back-to-back tracks.
8
+ // ---------------------------------------------------------------------------
9
+
10
+ const isBrowser = typeof window !== 'undefined';
11
+
12
+ let _audioContext: AudioContext | null | undefined = undefined;
13
+
14
+ function createAudioContext(): AudioContext | null {
15
+ if (!isBrowser) return null;
16
+ const Ctor =
17
+ (window as unknown as { AudioContext?: typeof AudioContext; webkitAudioContext?: typeof AudioContext })
18
+ .AudioContext ??
19
+ (window as unknown as { webkitAudioContext?: typeof AudioContext }).webkitAudioContext;
20
+ if (!Ctor) return null;
21
+ return new Ctor();
22
+ }
23
+
24
+ /**
25
+ * Return the shared AudioContext if it has already been created, otherwise null.
26
+ *
27
+ * Does NOT create the context — call resumeAudioContext() from a user-gesture
28
+ * handler to create and resume it. This prevents the browser warning:
29
+ * "An AudioContext was prevented from starting automatically."
30
+ */
31
+ export function getAudioContext(): AudioContext | null {
32
+ // undefined = never initialised (return null without creating)
33
+ // null = explicitly disabled or unavailable
34
+ // AudioContext = ready to use
35
+ if (_audioContext === undefined) return null;
36
+ return _audioContext;
37
+ }
38
+
39
+ /**
40
+ * Create (if needed) and resume the AudioContext.
41
+ * Must be called from a user-gesture handler (click, keydown, etc.).
42
+ * Safe to call multiple times — subsequent calls are cheap no-ops.
43
+ */
44
+ export function resumeAudioContext(): Promise<void> {
45
+ if (_audioContext === undefined) {
46
+ _audioContext = createAudioContext();
47
+ }
48
+ const ctx = _audioContext;
49
+ if (ctx && ctx.state === 'suspended') {
50
+ return ctx.resume();
51
+ }
52
+ return Promise.resolve();
53
+ }
54
+
55
+ /**
56
+ * Reset the cached AudioContext — used in tests so each test gets a fresh mock.
57
+ * @internal
58
+ */
59
+ export function _resetAudioContext(): void {
60
+ _audioContext = undefined;
61
+ }
62
+
63
+ /**
64
+ * Directly set the cached AudioContext — used in tests to inject a mock instance.
65
+ * @internal
66
+ */
67
+ export function _setAudioContext(ctx: AudioContext | null): void {
68
+ _audioContext = ctx;
69
+ }
@@ -0,0 +1,55 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Media Session API integration
3
+ //
4
+ // Provides OS-level media controls (lock screen, notification shade, keyboard
5
+ // media keys). Gracefully no-ops when the API is unavailable.
6
+ // ---------------------------------------------------------------------------
7
+
8
+ import type { TrackMetadata } from '../types';
9
+
10
+ const hasMediaSession =
11
+ typeof navigator !== 'undefined' && 'mediaSession' in navigator;
12
+
13
+ export interface MediaSessionHandlers {
14
+ onPlay: () => void;
15
+ onPause: () => void;
16
+ onNext: () => void;
17
+ onPrevious: () => void;
18
+ onSeek: (time: number) => void;
19
+ }
20
+
21
+ /** Register Media Session action handlers. Call once after construction. */
22
+ export function setupMediaSession(handlers: MediaSessionHandlers): void {
23
+ if (!hasMediaSession) return;
24
+ const { mediaSession } = navigator;
25
+ mediaSession.setActionHandler('play', handlers.onPlay);
26
+ mediaSession.setActionHandler('pause', handlers.onPause);
27
+ mediaSession.setActionHandler('nexttrack', handlers.onNext);
28
+ mediaSession.setActionHandler('previoustrack', handlers.onPrevious);
29
+ mediaSession.setActionHandler('seekto', (details) => {
30
+ if (details.seekTime != null) handlers.onSeek(details.seekTime);
31
+ });
32
+ }
33
+
34
+ /** Update the OS metadata display (title, artist, album, artwork). */
35
+ export function updateMediaSessionMetadata(metadata?: TrackMetadata): void {
36
+ if (!hasMediaSession) return;
37
+ if (!metadata) {
38
+ navigator.mediaSession.metadata = null;
39
+ return;
40
+ }
41
+ navigator.mediaSession.metadata = new MediaMetadata({
42
+ title: metadata.title ?? '',
43
+ artist: metadata.artist ?? '',
44
+ album: metadata.album ?? '',
45
+ artwork: metadata.artwork ?? [],
46
+ });
47
+ }
48
+
49
+ /** Sync the OS playback state indicator (playing / paused). */
50
+ export function updateMediaSessionPlaybackState(
51
+ isPlaying: boolean,
52
+ ): void {
53
+ if (!hasMediaSession) return;
54
+ navigator.mediaSession.playbackState = isPlaying ? 'playing' : 'paused';
55
+ }
package/CHANGELOG.md DELETED
@@ -1,29 +0,0 @@
1
- ### 2.2.3
2
- * Add option to specify `mode` for `fetch()` operations
3
-
4
- ### 2.2.2
5
- * Allow currentTrack to be undefined
6
-
7
- ### 2.2.1
8
- * Use `globalThis` instead of `window` to access `AudioContext`
9
-
10
- ### 2.2.0
11
- * Remove `tracks` from Queue constructor
12
- * Expose Queue.tracks as public
13
- * Transpile as commonjs module
14
-
15
- ### 2.1.0
16
- Update dependencies and code style
17
-
18
- * Format with prettier
19
- * Order methods and properties based on accessor type
20
- * Add explicit return types to functions
21
- * Remove deprecated seekToEnd() function
22
-
23
- ### 2.0.0
24
- * Convert to typescript
25
- * Fix a handful of bugs as part of the typescript conversion
26
-
27
- ### 1.0.0
28
-
29
- * Initial release - [Original source](https://github.com/RelistenNet/gapless.js)