@yetric/audioplayer 0.0.0-stage → 1.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.
package/dist/index.cjs ADDED
@@ -0,0 +1,1287 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ attachAudioPlayerMediaSession: () => attachAudioPlayerMediaSession,
24
+ attachAudioPlayerPersistence: () => attachAudioPlayerPersistence,
25
+ attachAudioPlayerProgressReporter: () => attachAudioPlayerProgressReporter,
26
+ createAudioPlayer: () => createAudioPlayer,
27
+ createLocalStoragePersistenceAdapter: () => createLocalStoragePersistenceAdapter
28
+ });
29
+ module.exports = __toCommonJS(index_exports);
30
+ var DEFAULT_PREVIOUS_RESTART_THRESHOLD_SECONDS = 5;
31
+ var DEFAULT_PERSISTENCE_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1e3;
32
+ var DEFAULT_PERSISTENCE_SAVE_INTERVAL_MS = 5e3;
33
+ var DEFAULT_PROGRESS_REPORT_INTERVAL_MS = 5e3;
34
+ var DEFAULT_MEDIA_SESSION_ARTWORK_SIZES = [96, 128, 192, 256, 384, 512];
35
+ var DEFAULT_MEDIA_SESSION_SEEK_OFFSET_SECONDS = 10;
36
+ var createInitialState = (options) => ({
37
+ status: "idle",
38
+ currentSource: null,
39
+ currentSourceId: null,
40
+ currentTime: 0,
41
+ duration: 0,
42
+ buffered: 0,
43
+ progress: {
44
+ currentTime: 0,
45
+ duration: 0,
46
+ buffered: 0,
47
+ playedFraction: 0,
48
+ bufferedFraction: 0
49
+ },
50
+ rate: options.initialRate ?? 1,
51
+ volume: options.initialVolume ?? 1,
52
+ muted: options.initialMuted ?? false,
53
+ queue: {
54
+ items: [],
55
+ itemIds: [],
56
+ position: {
57
+ currentIndex: -1,
58
+ currentSourceId: null,
59
+ length: 0,
60
+ repeatMode: options.initialRepeatMode ?? "off",
61
+ hasNext: false,
62
+ hasPrevious: false
63
+ }
64
+ },
65
+ error: null
66
+ });
67
+ var createPlayerError = (code, message, cause) => ({
68
+ code,
69
+ message,
70
+ cause
71
+ });
72
+ var isRuntimePlayerError = (value) => !!value && typeof value === "object" && "code" in value && "message" in value;
73
+ var normalizePlayerError = (fallbackCode, fallbackMessage, cause) => isRuntimePlayerError(cause) ? cause : createPlayerError(fallbackCode, fallbackMessage, cause);
74
+ var assertStableSource = (source) => {
75
+ if (!source.id || !source.src) {
76
+ throw createPlayerError(
77
+ "INVALID_QUEUE_ITEM",
78
+ "AudioSource requires stable id and src."
79
+ );
80
+ }
81
+ };
82
+ var isFiniteNumber = (value) => Number.isFinite(value) && !Number.isNaN(value);
83
+ var safeDuration = (audio, hint) => {
84
+ const duration = audio?.duration;
85
+ return duration !== void 0 && isFiniteNumber(duration) ? duration : hint ?? 0;
86
+ };
87
+ var safeBuffered = (audio) => {
88
+ if (!audio || audio.buffered.length === 0) {
89
+ return 0;
90
+ }
91
+ try {
92
+ return audio.buffered.end(audio.buffered.length - 1);
93
+ } catch {
94
+ return 0;
95
+ }
96
+ };
97
+ var cloneQueueState = (state) => ({
98
+ items: [...state.queue.items],
99
+ itemIds: [...state.queue.itemIds],
100
+ position: {
101
+ ...state.queue.position
102
+ }
103
+ });
104
+ var updateQueueFlags = (state) => {
105
+ const { items, position } = state.queue;
106
+ const { currentIndex, repeatMode } = position;
107
+ const lastIndex = items.length - 1;
108
+ const hasItems = items.length > 0;
109
+ const hasNext = !hasItems || currentIndex < 0 ? false : repeatMode === "all" ? items.length > 0 : currentIndex < lastIndex;
110
+ const hasPrevious = !hasItems || currentIndex < 0 ? false : repeatMode === "all" ? items.length > 0 : currentIndex > 0;
111
+ return {
112
+ ...state,
113
+ queue: {
114
+ ...state.queue,
115
+ itemIds: items.map((item) => item.id),
116
+ position: {
117
+ ...position,
118
+ currentSourceId: state.currentSource?.id ?? null,
119
+ length: items.length,
120
+ hasNext,
121
+ hasPrevious
122
+ }
123
+ }
124
+ };
125
+ };
126
+ var createAudioElement = () => {
127
+ if (typeof Audio === "undefined") {
128
+ return null;
129
+ }
130
+ return new Audio();
131
+ };
132
+ var createUnavailableAudioEngine = () => {
133
+ const unavailableError = () => createPlayerError(
134
+ "UNSUPPORTED_ENVIRONMENT",
135
+ "HTMLAudioElement is not available in this environment."
136
+ );
137
+ return {
138
+ available: false,
139
+ getSnapshot(durationHint) {
140
+ return {
141
+ currentTime: 0,
142
+ duration: durationHint ?? 0,
143
+ buffered: 0,
144
+ rate: 1,
145
+ volume: 1,
146
+ muted: false
147
+ };
148
+ },
149
+ getRuntimeError() {
150
+ return unavailableError();
151
+ },
152
+ on() {
153
+ return () => {
154
+ };
155
+ },
156
+ setSource() {
157
+ throw unavailableError();
158
+ },
159
+ clearSource() {
160
+ throw unavailableError();
161
+ },
162
+ load() {
163
+ throw unavailableError();
164
+ },
165
+ play() {
166
+ throw unavailableError();
167
+ },
168
+ pause() {
169
+ throw unavailableError();
170
+ },
171
+ setCurrentTime() {
172
+ throw unavailableError();
173
+ },
174
+ setPlaybackRate() {
175
+ throw unavailableError();
176
+ },
177
+ setVolume() {
178
+ throw unavailableError();
179
+ },
180
+ setMuted() {
181
+ throw unavailableError();
182
+ },
183
+ destroy() {
184
+ }
185
+ };
186
+ };
187
+ var createHtmlAudioEngine = () => {
188
+ const audio = createAudioElement();
189
+ if (!audio) {
190
+ return createUnavailableAudioEngine();
191
+ }
192
+ audio.preload = "metadata";
193
+ const clearSource = () => {
194
+ const element = audio;
195
+ if (typeof element.removeAttribute === "function") {
196
+ element.removeAttribute("src");
197
+ return;
198
+ }
199
+ audio.src = "";
200
+ };
201
+ return {
202
+ available: true,
203
+ getSnapshot(durationHint) {
204
+ return {
205
+ currentTime: audio.currentTime,
206
+ duration: safeDuration(audio, durationHint),
207
+ buffered: safeBuffered(audio),
208
+ rate: audio.playbackRate,
209
+ volume: audio.volume,
210
+ muted: audio.muted
211
+ };
212
+ },
213
+ getRuntimeError() {
214
+ return audio.error;
215
+ },
216
+ on(eventName, listener) {
217
+ audio.addEventListener(eventName, listener);
218
+ return () => {
219
+ audio.removeEventListener(eventName, listener);
220
+ };
221
+ },
222
+ setSource(src) {
223
+ audio.src = src;
224
+ },
225
+ clearSource() {
226
+ clearSource();
227
+ },
228
+ load() {
229
+ audio.load();
230
+ },
231
+ play() {
232
+ return audio.play();
233
+ },
234
+ pause() {
235
+ audio.pause();
236
+ },
237
+ setCurrentTime(time) {
238
+ audio.currentTime = time;
239
+ },
240
+ setPlaybackRate(rate) {
241
+ audio.playbackRate = rate;
242
+ },
243
+ setVolume(volume) {
244
+ audio.volume = volume;
245
+ },
246
+ setMuted(muted) {
247
+ audio.muted = muted;
248
+ },
249
+ destroy() {
250
+ audio.pause();
251
+ clearSource();
252
+ audio.load();
253
+ }
254
+ };
255
+ };
256
+ var isBrowserStorageAvailable = () => typeof window !== "undefined" && typeof window.localStorage !== "undefined";
257
+ var isPersistedPlayerState = (value) => {
258
+ if (!value || typeof value !== "object") {
259
+ return false;
260
+ }
261
+ const candidate = value;
262
+ return candidate.version === 1 && typeof candidate.savedAt === "number" && "currentSource" in candidate && typeof candidate.currentTime === "number" && typeof candidate.rate === "number" && typeof candidate.volume === "number" && typeof candidate.muted === "boolean" && !!candidate.queue && Array.isArray(candidate.queue.items) && !!candidate.queue.position;
263
+ };
264
+ var toPersistedPlayerState = (state) => ({
265
+ version: 1,
266
+ savedAt: Date.now(),
267
+ currentSource: state.currentSource,
268
+ currentSourceId: state.currentSourceId,
269
+ currentTime: state.currentTime,
270
+ rate: state.rate,
271
+ volume: state.volume,
272
+ muted: state.muted,
273
+ queue: {
274
+ items: [...state.queue.items],
275
+ position: {
276
+ currentIndex: state.queue.position.currentIndex,
277
+ currentSourceId: state.queue.position.currentSourceId,
278
+ repeatMode: state.queue.position.repeatMode
279
+ }
280
+ }
281
+ });
282
+ var toProgressSnapshot = (state) => ({
283
+ source: state.currentSource,
284
+ sourceId: state.currentSourceId,
285
+ currentTime: state.currentTime,
286
+ duration: state.duration,
287
+ playedFraction: state.progress.playedFraction,
288
+ status: state.status,
289
+ queue: {
290
+ ...state.queue.position
291
+ }
292
+ });
293
+ var hasMediaSession = () => typeof navigator !== "undefined" && "mediaSession" in navigator;
294
+ var createMediaSessionArtwork = (source, sizes) => {
295
+ if (!source?.artwork) {
296
+ return [];
297
+ }
298
+ return sizes.map((size) => ({
299
+ src: source.artwork,
300
+ sizes: `${size}x${size}`
301
+ }));
302
+ };
303
+ var createLocalStoragePersistenceAdapter = (key) => ({
304
+ load() {
305
+ if (!isBrowserStorageAvailable()) {
306
+ return null;
307
+ }
308
+ try {
309
+ const raw = window.localStorage.getItem(key);
310
+ if (!raw) {
311
+ return null;
312
+ }
313
+ const parsed = JSON.parse(raw);
314
+ return isPersistedPlayerState(parsed) ? parsed : null;
315
+ } catch {
316
+ return null;
317
+ }
318
+ },
319
+ save(state) {
320
+ if (!isBrowserStorageAvailable()) {
321
+ return;
322
+ }
323
+ window.localStorage.setItem(key, JSON.stringify(state));
324
+ },
325
+ clear() {
326
+ if (!isBrowserStorageAvailable()) {
327
+ return;
328
+ }
329
+ window.localStorage.removeItem(key);
330
+ }
331
+ });
332
+ var createAudioPlayer = (options = {}) => {
333
+ let state = updateQueueFlags(createInitialState(options));
334
+ let destroyed = false;
335
+ let shouldAutoplayAfterLoad = false;
336
+ let suppressPauseEvent = false;
337
+ const engine = createHtmlAudioEngine();
338
+ const subscribers = /* @__PURE__ */ new Set();
339
+ const eventListeners = {
340
+ sourcechange: /* @__PURE__ */ new Set(),
341
+ queuechange: /* @__PURE__ */ new Set(),
342
+ play: /* @__PURE__ */ new Set(),
343
+ pause: /* @__PURE__ */ new Set(),
344
+ seeking: /* @__PURE__ */ new Set(),
345
+ seeked: /* @__PURE__ */ new Set(),
346
+ timeupdate: /* @__PURE__ */ new Set(),
347
+ ratechange: /* @__PURE__ */ new Set(),
348
+ volumechange: /* @__PURE__ */ new Set(),
349
+ ended: /* @__PURE__ */ new Set(),
350
+ error: /* @__PURE__ */ new Set(),
351
+ destroy: /* @__PURE__ */ new Set()
352
+ };
353
+ const getState = () => state;
354
+ const publishState = () => {
355
+ for (const subscriber of subscribers) {
356
+ subscriber(state);
357
+ }
358
+ };
359
+ const emit = (eventName, event) => {
360
+ for (const listener of eventListeners[eventName]) {
361
+ listener(event);
362
+ }
363
+ };
364
+ const setState = (updater) => {
365
+ state = updateQueueFlags(updater(state));
366
+ publishState();
367
+ return state;
368
+ };
369
+ const setError = (error) => {
370
+ shouldAutoplayAfterLoad = false;
371
+ const nextState = setState((currentState) => ({
372
+ ...currentState,
373
+ status: "error",
374
+ error: {
375
+ code: error.code,
376
+ message: error.message
377
+ }
378
+ }));
379
+ emit("error", { error, state: nextState });
380
+ };
381
+ const clearError = () => {
382
+ if (!state.error) {
383
+ return;
384
+ }
385
+ setState((currentState) => ({
386
+ ...currentState,
387
+ error: null
388
+ }));
389
+ };
390
+ const syncAudioSnapshot = (statusOverride) => {
391
+ const snapshot = engine.getSnapshot(state.currentSource?.durationHint);
392
+ const currentTime = snapshot.currentTime;
393
+ const duration = snapshot.duration;
394
+ const buffered = snapshot.buffered;
395
+ const playedFraction = duration > 0 ? Math.min(1, Math.max(0, currentTime / duration)) : 0;
396
+ const bufferedFraction = duration > 0 ? Math.min(1, Math.max(0, buffered / duration)) : 0;
397
+ const nextState = setState((currentState) => ({
398
+ ...currentState,
399
+ currentSourceId: currentState.currentSource?.id ?? null,
400
+ currentTime,
401
+ duration,
402
+ buffered,
403
+ progress: {
404
+ currentTime,
405
+ duration,
406
+ buffered,
407
+ playedFraction,
408
+ bufferedFraction
409
+ },
410
+ rate: snapshot.rate,
411
+ volume: snapshot.volume,
412
+ muted: snapshot.muted,
413
+ status: statusOverride ?? currentState.status,
414
+ error: statusOverride === "error" ? currentState.error : null
415
+ }));
416
+ return nextState;
417
+ };
418
+ const findQueueIndexById = (id) => id ? state.queue.items.findIndex((item) => item.id === id) : -1;
419
+ const resolveNextQueueIndex = (mode) => {
420
+ const { items, position } = state.queue;
421
+ const { currentIndex, repeatMode } = position;
422
+ if (items.length === 0 || currentIndex < 0) {
423
+ return null;
424
+ }
425
+ if (repeatMode === "one") {
426
+ return currentIndex;
427
+ }
428
+ if (currentIndex < items.length - 1) {
429
+ return currentIndex + 1;
430
+ }
431
+ if (repeatMode === "all") {
432
+ return 0;
433
+ }
434
+ return mode === "ended" ? -1 : null;
435
+ };
436
+ const resolvePreviousQueueIndex = () => {
437
+ const { items, position } = state.queue;
438
+ const { currentIndex, repeatMode } = position;
439
+ if (items.length === 0 || currentIndex < 0) {
440
+ return null;
441
+ }
442
+ if (currentIndex > 0) {
443
+ return currentIndex - 1;
444
+ }
445
+ if (repeatMode === "all") {
446
+ return items.length - 1;
447
+ }
448
+ return null;
449
+ };
450
+ const updateCurrentSource = (source, nextStatus) => {
451
+ const queueIndex = findQueueIndexById(source?.id);
452
+ const nextState = setState((currentState) => ({
453
+ ...currentState,
454
+ currentSource: source,
455
+ currentSourceId: source?.id ?? null,
456
+ currentTime: 0,
457
+ duration: source?.durationHint ?? 0,
458
+ buffered: 0,
459
+ progress: {
460
+ currentTime: 0,
461
+ duration: source?.durationHint ?? 0,
462
+ buffered: 0,
463
+ playedFraction: 0,
464
+ bufferedFraction: 0
465
+ },
466
+ status: nextStatus,
467
+ error: null,
468
+ queue: {
469
+ ...currentState.queue,
470
+ position: {
471
+ ...currentState.queue.position,
472
+ currentIndex: queueIndex,
473
+ currentSourceId: source?.id ?? null
474
+ }
475
+ }
476
+ }));
477
+ emit("sourcechange", { source, state: nextState });
478
+ return nextState;
479
+ };
480
+ const setQueueState = (items, currentIndex) => setState((currentState) => ({
481
+ ...currentState,
482
+ queue: {
483
+ ...currentState.queue,
484
+ items,
485
+ position: {
486
+ ...currentState.queue.position,
487
+ currentIndex
488
+ }
489
+ }
490
+ }));
491
+ const emitQueueChange = (nextState) => {
492
+ emit("queuechange", { queue: cloneQueueState(nextState), state: nextState });
493
+ };
494
+ const attachSourceToAudio = (source, autoplayAfterLoad) => {
495
+ shouldAutoplayAfterLoad = autoplayAfterLoad;
496
+ suppressPauseEvent = true;
497
+ try {
498
+ engine.pause();
499
+ } finally {
500
+ suppressPauseEvent = false;
501
+ }
502
+ engine.setSource(source.src);
503
+ engine.setCurrentTime(0);
504
+ engine.setPlaybackRate(state.rate);
505
+ engine.setVolume(state.volume);
506
+ engine.setMuted(state.muted);
507
+ engine.load();
508
+ };
509
+ const loadInternal = async (source, autoplayAfterLoad = false) => {
510
+ assertStableSource(source);
511
+ updateCurrentSource(source, "loading");
512
+ attachSourceToAudio(source, autoplayAfterLoad);
513
+ };
514
+ const selectQueueIndex = async (index, autoplayAfterLoad = false) => {
515
+ const item = state.queue.items[index] ?? null;
516
+ if (!item) {
517
+ return null;
518
+ }
519
+ await loadInternal(item, autoplayAfterLoad);
520
+ const nextState = setQueueState(state.queue.items, index);
521
+ emitQueueChange(nextState);
522
+ return item;
523
+ };
524
+ const playLoadedAudio = async () => {
525
+ try {
526
+ await engine.play();
527
+ } catch (cause) {
528
+ setError(createPlayerError("PLAY_ERROR", "Failed to start playback.", cause));
529
+ }
530
+ };
531
+ const handlePlaybackEnded = async () => {
532
+ if (destroyed) {
533
+ return;
534
+ }
535
+ if (state.queue.items.length > 0 && state.queue.position.currentIndex >= 0) {
536
+ const nextIndex = resolveNextQueueIndex("ended");
537
+ if (nextIndex === -1 || nextIndex === null) {
538
+ const endedState2 = syncAudioSnapshot("ended");
539
+ emit("ended", { source: endedState2.currentSource, state: endedState2 });
540
+ return;
541
+ }
542
+ await selectQueueIndex(nextIndex, true);
543
+ return;
544
+ }
545
+ const endedState = syncAudioSnapshot("ended");
546
+ emit("ended", { source: endedState.currentSource, state: endedState });
547
+ };
548
+ const detachEngineListeners = [
549
+ engine.on("loadedmetadata", () => {
550
+ if (destroyed || !state.currentSource) {
551
+ return;
552
+ }
553
+ syncAudioSnapshot("ready");
554
+ if (shouldAutoplayAfterLoad) {
555
+ shouldAutoplayAfterLoad = false;
556
+ void playLoadedAudio();
557
+ }
558
+ }),
559
+ engine.on("timeupdate", () => {
560
+ if (destroyed) {
561
+ return;
562
+ }
563
+ const nextState = syncAudioSnapshot(
564
+ state.status === "ended" ? "playing" : void 0
565
+ );
566
+ emit("timeupdate", {
567
+ currentTime: nextState.currentTime,
568
+ duration: nextState.duration,
569
+ state: nextState
570
+ });
571
+ }),
572
+ engine.on("progress", () => {
573
+ if (!destroyed) {
574
+ syncAudioSnapshot();
575
+ }
576
+ }),
577
+ engine.on("play", () => {
578
+ if (destroyed) {
579
+ return;
580
+ }
581
+ const nextState = syncAudioSnapshot("playing");
582
+ emit("play", { source: nextState.currentSource, state: nextState });
583
+ }),
584
+ engine.on("pause", () => {
585
+ if (destroyed || suppressPauseEvent) {
586
+ return;
587
+ }
588
+ const nextStatus = state.status === "ended" ? "ended" : state.currentSource ? "paused" : "idle";
589
+ const nextState = syncAudioSnapshot(nextStatus);
590
+ emit("pause", { source: nextState.currentSource, state: nextState });
591
+ }),
592
+ engine.on("seeked", () => {
593
+ if (destroyed) {
594
+ return;
595
+ }
596
+ const nextState = syncAudioSnapshot();
597
+ emit("seeked", { currentTime: nextState.currentTime, state: nextState });
598
+ emit("timeupdate", {
599
+ currentTime: nextState.currentTime,
600
+ duration: nextState.duration,
601
+ state: nextState
602
+ });
603
+ }),
604
+ engine.on("ratechange", () => {
605
+ if (destroyed) {
606
+ return;
607
+ }
608
+ const nextState = syncAudioSnapshot();
609
+ emit("ratechange", { rate: nextState.rate, state: nextState });
610
+ }),
611
+ engine.on("volumechange", () => {
612
+ if (destroyed) {
613
+ return;
614
+ }
615
+ const nextState = syncAudioSnapshot();
616
+ emit("volumechange", {
617
+ volume: nextState.volume,
618
+ muted: nextState.muted,
619
+ state: nextState
620
+ });
621
+ }),
622
+ engine.on("ended", () => {
623
+ void handlePlaybackEnded();
624
+ }),
625
+ engine.on("error", () => {
626
+ if (destroyed) {
627
+ return;
628
+ }
629
+ setError(
630
+ createPlayerError(
631
+ "LOAD_ERROR",
632
+ "Audio element reported a playback error.",
633
+ engine.getRuntimeError()
634
+ )
635
+ );
636
+ })
637
+ ];
638
+ return {
639
+ getState,
640
+ subscribe(listener) {
641
+ subscribers.add(listener);
642
+ listener(state);
643
+ return () => {
644
+ subscribers.delete(listener);
645
+ };
646
+ },
647
+ on(eventName, listener) {
648
+ eventListeners[eventName].add(
649
+ listener
650
+ );
651
+ return () => {
652
+ eventListeners[eventName].delete(
653
+ listener
654
+ );
655
+ };
656
+ },
657
+ async load(source) {
658
+ try {
659
+ await loadInternal(source);
660
+ } catch (cause) {
661
+ setError(
662
+ normalizePlayerError("LOAD_ERROR", "Failed to load source.", cause)
663
+ );
664
+ }
665
+ },
666
+ async play(source) {
667
+ try {
668
+ if (source) {
669
+ await loadInternal(source, true);
670
+ return;
671
+ }
672
+ if (!state.currentSource) {
673
+ setError(
674
+ createPlayerError(
675
+ "NO_ACTIVE_SOURCE",
676
+ "Cannot play without an active source."
677
+ )
678
+ );
679
+ return;
680
+ }
681
+ if (state.status === "loading") {
682
+ shouldAutoplayAfterLoad = true;
683
+ return;
684
+ }
685
+ await playLoadedAudio();
686
+ } catch (cause) {
687
+ setError(normalizePlayerError("PLAY_ERROR", "Failed to start playback.", cause));
688
+ }
689
+ },
690
+ pause() {
691
+ if (!engine.available) {
692
+ return;
693
+ }
694
+ shouldAutoplayAfterLoad = false;
695
+ engine.pause();
696
+ },
697
+ async toggle() {
698
+ if (state.status === "playing") {
699
+ this.pause();
700
+ return;
701
+ }
702
+ await this.play();
703
+ },
704
+ unload() {
705
+ shouldAutoplayAfterLoad = false;
706
+ if (engine.available) {
707
+ suppressPauseEvent = true;
708
+ try {
709
+ engine.pause();
710
+ } finally {
711
+ suppressPauseEvent = false;
712
+ }
713
+ engine.clearSource();
714
+ engine.load();
715
+ }
716
+ const nextState = updateCurrentSource(null, "idle");
717
+ emitQueueChange(nextState);
718
+ },
719
+ seek(time) {
720
+ if (!state.currentSource) {
721
+ setError(
722
+ createPlayerError("SEEK_ERROR", "Cannot seek without an active source.")
723
+ );
724
+ return;
725
+ }
726
+ const max = engine.getSnapshot(state.currentSource.durationHint).duration;
727
+ const normalized = Math.max(
728
+ 0,
729
+ max > 0 ? Math.min(time, max) : isFiniteNumber(time) ? time : 0
730
+ );
731
+ emit("seeking", { from: state.currentTime, to: normalized, state });
732
+ try {
733
+ engine.setCurrentTime(normalized);
734
+ } catch (cause) {
735
+ setError(normalizePlayerError("SEEK_ERROR", "Failed to seek audio.", cause));
736
+ }
737
+ },
738
+ setRate(rate) {
739
+ if (!engine.available) {
740
+ setError(
741
+ createPlayerError("INVALID_RATE", "Audio element is unavailable.")
742
+ );
743
+ return;
744
+ }
745
+ if (!isFiniteNumber(rate) || rate <= 0) {
746
+ setError(
747
+ createPlayerError("INVALID_RATE", "Playback rate must be greater than 0.")
748
+ );
749
+ return;
750
+ }
751
+ engine.setPlaybackRate(rate);
752
+ clearError();
753
+ syncAudioSnapshot();
754
+ },
755
+ setVolume(volume) {
756
+ if (!engine.available) {
757
+ setError(
758
+ createPlayerError("INVALID_VOLUME", "Audio element is unavailable.")
759
+ );
760
+ return;
761
+ }
762
+ if (!isFiniteNumber(volume) || volume < 0 || volume > 1) {
763
+ setError(
764
+ createPlayerError("INVALID_VOLUME", "Volume must be between 0 and 1.")
765
+ );
766
+ return;
767
+ }
768
+ engine.setVolume(volume);
769
+ clearError();
770
+ syncAudioSnapshot();
771
+ },
772
+ setMuted(muted) {
773
+ if (!engine.available) {
774
+ return;
775
+ }
776
+ engine.setMuted(muted);
777
+ clearError();
778
+ syncAudioSnapshot();
779
+ },
780
+ async setQueue(items, queueOptions = {}) {
781
+ try {
782
+ for (const item of items) {
783
+ assertStableSource(item);
784
+ }
785
+ const startIndex = queueOptions.startAtId === void 0 ? items.length > 0 ? 0 : -1 : items.findIndex((item) => item.id === queueOptions.startAtId);
786
+ if (queueOptions.startAtId && startIndex < 0) {
787
+ throw createPlayerError(
788
+ "INVALID_QUEUE_ITEM",
789
+ `Queue start item ${queueOptions.startAtId} was not found.`
790
+ );
791
+ }
792
+ const nextState = setState((currentState) => ({
793
+ ...currentState,
794
+ currentSource: startIndex >= 0 ? items[startIndex] ?? null : null,
795
+ currentSourceId: startIndex >= 0 ? items[startIndex]?.id ?? null : null,
796
+ currentTime: 0,
797
+ duration: startIndex >= 0 ? items[startIndex]?.durationHint ?? 0 : 0,
798
+ buffered: 0,
799
+ progress: {
800
+ currentTime: 0,
801
+ duration: startIndex >= 0 ? items[startIndex]?.durationHint ?? 0 : 0,
802
+ buffered: 0,
803
+ playedFraction: 0,
804
+ bufferedFraction: 0
805
+ },
806
+ status: startIndex >= 0 ? "ready" : "idle",
807
+ error: null,
808
+ queue: {
809
+ ...currentState.queue,
810
+ items: [...items],
811
+ position: {
812
+ ...currentState.queue.position,
813
+ currentIndex: startIndex,
814
+ currentSourceId: startIndex >= 0 ? items[startIndex]?.id ?? null : null
815
+ }
816
+ }
817
+ }));
818
+ emitQueueChange(nextState);
819
+ emit("sourcechange", {
820
+ source: nextState.currentSource,
821
+ state: nextState
822
+ });
823
+ if (nextState.currentSource) {
824
+ await loadInternal(nextState.currentSource, queueOptions.autoplay === true);
825
+ const syncedState = setQueueState(items, startIndex);
826
+ emitQueueChange(syncedState);
827
+ } else if (engine.available) {
828
+ shouldAutoplayAfterLoad = false;
829
+ suppressPauseEvent = true;
830
+ engine.pause();
831
+ suppressPauseEvent = false;
832
+ engine.clearSource();
833
+ engine.load();
834
+ }
835
+ } catch (cause) {
836
+ setError(
837
+ normalizePlayerError(
838
+ "INVALID_QUEUE_ITEM",
839
+ "Failed to set queue.",
840
+ cause
841
+ )
842
+ );
843
+ }
844
+ },
845
+ appendToQueue(items) {
846
+ try {
847
+ for (const item of items) {
848
+ assertStableSource(item);
849
+ }
850
+ const nextState = setState((currentState) => ({
851
+ ...currentState,
852
+ queue: {
853
+ ...currentState.queue,
854
+ items: [...currentState.queue.items, ...items]
855
+ }
856
+ }));
857
+ emitQueueChange(nextState);
858
+ } catch (cause) {
859
+ setError(
860
+ normalizePlayerError(
861
+ "INVALID_QUEUE_ITEM",
862
+ "Failed to append queue items.",
863
+ cause
864
+ )
865
+ );
866
+ }
867
+ },
868
+ removeFromQueue(id) {
869
+ const nextItems = state.queue.items.filter((item) => item.id !== id);
870
+ const nextIndex = nextItems.findIndex(
871
+ (item) => item.id === state.currentSource?.id
872
+ );
873
+ const nextState = setState((currentState) => ({
874
+ ...currentState,
875
+ queue: {
876
+ ...currentState.queue,
877
+ items: nextItems,
878
+ position: {
879
+ ...currentState.queue.position,
880
+ currentIndex: nextIndex,
881
+ currentSourceId: nextIndex >= 0 ? nextItems[nextIndex]?.id ?? null : null
882
+ }
883
+ }
884
+ }));
885
+ emitQueueChange(nextState);
886
+ },
887
+ clearQueue() {
888
+ const nextState = setState((currentState) => ({
889
+ ...currentState,
890
+ queue: {
891
+ ...currentState.queue,
892
+ items: [],
893
+ position: {
894
+ ...currentState.queue.position,
895
+ currentIndex: -1,
896
+ currentSourceId: null
897
+ }
898
+ }
899
+ }));
900
+ emitQueueChange(nextState);
901
+ },
902
+ async next() {
903
+ const nextIndex = resolveNextQueueIndex("manual");
904
+ if (nextIndex === null) {
905
+ return;
906
+ }
907
+ await selectQueueIndex(nextIndex, true);
908
+ },
909
+ async previous(previousOptions = {}) {
910
+ if (!state.currentSource) {
911
+ return;
912
+ }
913
+ const threshold = previousOptions.restartThresholdSeconds ?? DEFAULT_PREVIOUS_RESTART_THRESHOLD_SECONDS;
914
+ if (state.currentTime > threshold) {
915
+ this.seek(0);
916
+ return;
917
+ }
918
+ const previousIndex = resolvePreviousQueueIndex();
919
+ if (previousIndex === null) {
920
+ this.seek(0);
921
+ return;
922
+ }
923
+ await selectQueueIndex(previousIndex, true);
924
+ },
925
+ setRepeatMode(mode) {
926
+ const nextState = setState((currentState) => ({
927
+ ...currentState,
928
+ queue: {
929
+ ...currentState.queue,
930
+ position: {
931
+ ...currentState.queue.position,
932
+ repeatMode: mode
933
+ }
934
+ }
935
+ }));
936
+ emitQueueChange(nextState);
937
+ },
938
+ destroy() {
939
+ if (destroyed) {
940
+ return;
941
+ }
942
+ destroyed = true;
943
+ shouldAutoplayAfterLoad = false;
944
+ for (const detach of detachEngineListeners) {
945
+ detach();
946
+ }
947
+ suppressPauseEvent = true;
948
+ engine.destroy();
949
+ suppressPauseEvent = false;
950
+ const nextState = setState(() => createInitialState(options));
951
+ emit("destroy", { state: nextState });
952
+ subscribers.clear();
953
+ for (const eventName of Object.keys(eventListeners)) {
954
+ eventListeners[eventName].clear();
955
+ }
956
+ }
957
+ };
958
+ };
959
+ var attachAudioPlayerPersistence = (player, options) => {
960
+ const maxAgeMs = options.maxAgeMs ?? DEFAULT_PERSISTENCE_MAX_AGE_MS;
961
+ const saveIntervalMs = options.saveIntervalMs ?? DEFAULT_PERSISTENCE_SAVE_INTERVAL_MS;
962
+ let destroyed = false;
963
+ let playIntervalId = null;
964
+ const save = () => {
965
+ if (destroyed) {
966
+ return;
967
+ }
968
+ const state = player.getState();
969
+ if (!state.currentSource) {
970
+ options.adapter.clear();
971
+ return;
972
+ }
973
+ options.adapter.save(toPersistedPlayerState(state));
974
+ };
975
+ const clear = () => {
976
+ options.adapter.clear();
977
+ };
978
+ const stopPlayInterval = () => {
979
+ if (playIntervalId !== null) {
980
+ clearInterval(playIntervalId);
981
+ playIntervalId = null;
982
+ }
983
+ };
984
+ const startPlayInterval = () => {
985
+ if (playIntervalId !== null) {
986
+ return;
987
+ }
988
+ playIntervalId = setInterval(() => {
989
+ save();
990
+ }, saveIntervalMs);
991
+ };
992
+ const waitForRestoredSource = (sourceId) => new Promise((resolve) => {
993
+ let settled = false;
994
+ let unsubscribe;
995
+ unsubscribe = player.subscribe((state) => {
996
+ if (settled || state.currentSourceId !== sourceId) {
997
+ return;
998
+ }
999
+ if (state.status === "loading") {
1000
+ return;
1001
+ }
1002
+ settled = true;
1003
+ unsubscribe?.();
1004
+ resolve();
1005
+ });
1006
+ if (settled) {
1007
+ unsubscribe();
1008
+ }
1009
+ });
1010
+ const restore = async () => {
1011
+ const persisted = options.adapter.load();
1012
+ if (!persisted) {
1013
+ return null;
1014
+ }
1015
+ if (Date.now() - persisted.savedAt > maxAgeMs) {
1016
+ options.adapter.clear();
1017
+ return null;
1018
+ }
1019
+ player.setRepeatMode(persisted.queue.position.repeatMode);
1020
+ player.setVolume(persisted.volume);
1021
+ player.setMuted(persisted.muted);
1022
+ player.setRate(persisted.rate);
1023
+ if (persisted.queue.items.length > 0) {
1024
+ await player.setQueue(persisted.queue.items, {
1025
+ startAtId: persisted.queue.position.currentSourceId ?? void 0
1026
+ });
1027
+ } else if (persisted.currentSource) {
1028
+ await player.load(persisted.currentSource);
1029
+ } else {
1030
+ return persisted;
1031
+ }
1032
+ if (persisted.currentSourceId) {
1033
+ await waitForRestoredSource(persisted.currentSourceId);
1034
+ }
1035
+ if (persisted.currentTime > 0) {
1036
+ player.seek(persisted.currentTime);
1037
+ }
1038
+ return persisted;
1039
+ };
1040
+ const stateUnsubscribe = player.subscribe((state) => {
1041
+ if (state.status === "playing") {
1042
+ startPlayInterval();
1043
+ return;
1044
+ }
1045
+ stopPlayInterval();
1046
+ });
1047
+ const pauseUnsubscribe = player.on("pause", () => {
1048
+ save();
1049
+ });
1050
+ const endedUnsubscribe = player.on("ended", () => {
1051
+ save();
1052
+ });
1053
+ const sourceUnsubscribe = player.on("sourcechange", () => {
1054
+ save();
1055
+ });
1056
+ const queueUnsubscribe = player.on("queuechange", () => {
1057
+ save();
1058
+ });
1059
+ const rateUnsubscribe = player.on("ratechange", () => {
1060
+ save();
1061
+ });
1062
+ const volumeUnsubscribe = player.on("volumechange", () => {
1063
+ save();
1064
+ });
1065
+ const beforeUnloadHandler = () => {
1066
+ save();
1067
+ };
1068
+ const visibilityHandler = () => {
1069
+ if (typeof document === "undefined") {
1070
+ return;
1071
+ }
1072
+ if (document.visibilityState === "hidden") {
1073
+ save();
1074
+ }
1075
+ };
1076
+ if (typeof window !== "undefined") {
1077
+ window.addEventListener("beforeunload", beforeUnloadHandler);
1078
+ }
1079
+ if (typeof document !== "undefined") {
1080
+ document.addEventListener("visibilitychange", visibilityHandler);
1081
+ }
1082
+ if (options.autoRestore !== false) {
1083
+ void restore();
1084
+ }
1085
+ return {
1086
+ restore,
1087
+ save,
1088
+ clear,
1089
+ destroy() {
1090
+ destroyed = true;
1091
+ stopPlayInterval();
1092
+ stateUnsubscribe();
1093
+ pauseUnsubscribe();
1094
+ endedUnsubscribe();
1095
+ sourceUnsubscribe();
1096
+ queueUnsubscribe();
1097
+ rateUnsubscribe();
1098
+ volumeUnsubscribe();
1099
+ if (typeof window !== "undefined") {
1100
+ window.removeEventListener("beforeunload", beforeUnloadHandler);
1101
+ }
1102
+ if (typeof document !== "undefined") {
1103
+ document.removeEventListener("visibilitychange", visibilityHandler);
1104
+ }
1105
+ }
1106
+ };
1107
+ };
1108
+ var attachAudioPlayerProgressReporter = (player, options) => {
1109
+ const intervalMs = options.intervalMs ?? DEFAULT_PROGRESS_REPORT_INTERVAL_MS;
1110
+ let destroyed = false;
1111
+ let intervalId = null;
1112
+ const report = () => {
1113
+ if (destroyed) {
1114
+ return;
1115
+ }
1116
+ const state = player.getState();
1117
+ if (!state.currentSource) {
1118
+ return;
1119
+ }
1120
+ options.onReport(toProgressSnapshot(state));
1121
+ };
1122
+ const stopInterval = () => {
1123
+ if (intervalId !== null) {
1124
+ clearInterval(intervalId);
1125
+ intervalId = null;
1126
+ }
1127
+ };
1128
+ const startInterval = () => {
1129
+ if (intervalId !== null) {
1130
+ return;
1131
+ }
1132
+ intervalId = setInterval(() => {
1133
+ report();
1134
+ }, intervalMs);
1135
+ };
1136
+ const stateUnsubscribe = player.subscribe((state) => {
1137
+ if (state.status === "playing") {
1138
+ startInterval();
1139
+ return;
1140
+ }
1141
+ stopInterval();
1142
+ });
1143
+ const pauseUnsubscribe = player.on("pause", () => {
1144
+ report();
1145
+ });
1146
+ const endedUnsubscribe = player.on("ended", () => {
1147
+ report();
1148
+ });
1149
+ const seekedUnsubscribe = player.on("seeked", () => {
1150
+ report();
1151
+ });
1152
+ const sourceUnsubscribe = player.on("sourcechange", () => {
1153
+ report();
1154
+ });
1155
+ return {
1156
+ report,
1157
+ destroy() {
1158
+ destroyed = true;
1159
+ stopInterval();
1160
+ stateUnsubscribe();
1161
+ pauseUnsubscribe();
1162
+ endedUnsubscribe();
1163
+ seekedUnsubscribe();
1164
+ sourceUnsubscribe();
1165
+ }
1166
+ };
1167
+ };
1168
+ var attachAudioPlayerMediaSession = (player, options = {}) => {
1169
+ if (!hasMediaSession()) {
1170
+ return {
1171
+ sync() {
1172
+ },
1173
+ destroy() {
1174
+ }
1175
+ };
1176
+ }
1177
+ const artworkSizes = options.artworkSizes ?? DEFAULT_MEDIA_SESSION_ARTWORK_SIZES;
1178
+ const seekOffsetSeconds = options.seekOffsetSeconds ?? DEFAULT_MEDIA_SESSION_SEEK_OFFSET_SECONDS;
1179
+ let destroyed = false;
1180
+ const sync = () => {
1181
+ if (destroyed || !hasMediaSession()) {
1182
+ return;
1183
+ }
1184
+ const state = player.getState();
1185
+ const source = state.currentSource;
1186
+ navigator.mediaSession.metadata = source ? new MediaMetadata({
1187
+ title: source.title ?? "",
1188
+ artist: source.artist ?? "",
1189
+ album: source.album ?? "",
1190
+ artwork: createMediaSessionArtwork(source, artworkSizes)
1191
+ }) : null;
1192
+ navigator.mediaSession.playbackState = state.status === "playing" ? "playing" : state.currentSource ? "paused" : "none";
1193
+ if (state.currentSource && state.duration > 0) {
1194
+ try {
1195
+ navigator.mediaSession.setPositionState({
1196
+ duration: state.duration,
1197
+ playbackRate: state.rate,
1198
+ position: Math.min(state.currentTime, state.duration)
1199
+ });
1200
+ } catch {
1201
+ }
1202
+ } else {
1203
+ try {
1204
+ navigator.mediaSession.setPositionState();
1205
+ } catch {
1206
+ }
1207
+ }
1208
+ };
1209
+ const setActionHandler = (action, handler) => {
1210
+ try {
1211
+ navigator.mediaSession.setActionHandler(action, handler);
1212
+ } catch {
1213
+ }
1214
+ };
1215
+ setActionHandler("play", () => {
1216
+ void player.play();
1217
+ });
1218
+ setActionHandler("pause", () => {
1219
+ player.pause();
1220
+ });
1221
+ setActionHandler("seekbackward", () => {
1222
+ const state = player.getState();
1223
+ player.seek(Math.max(0, state.currentTime - seekOffsetSeconds));
1224
+ });
1225
+ setActionHandler("seekforward", () => {
1226
+ const state = player.getState();
1227
+ const max = state.duration > 0 ? state.duration : state.currentTime + seekOffsetSeconds;
1228
+ player.seek(Math.min(max, state.currentTime + seekOffsetSeconds));
1229
+ });
1230
+ setActionHandler("previoustrack", () => {
1231
+ void player.previous();
1232
+ });
1233
+ setActionHandler("nexttrack", () => {
1234
+ void player.next();
1235
+ });
1236
+ setActionHandler("seekto", (details) => {
1237
+ if (details.seekTime === void 0) {
1238
+ return;
1239
+ }
1240
+ player.seek(details.seekTime);
1241
+ });
1242
+ setActionHandler("stop", () => {
1243
+ player.pause();
1244
+ });
1245
+ const stateUnsubscribe = player.subscribe(() => {
1246
+ sync();
1247
+ });
1248
+ sync();
1249
+ return {
1250
+ sync,
1251
+ destroy() {
1252
+ destroyed = true;
1253
+ stateUnsubscribe();
1254
+ if (!hasMediaSession()) {
1255
+ return;
1256
+ }
1257
+ const actions = [
1258
+ "play",
1259
+ "pause",
1260
+ "seekbackward",
1261
+ "seekforward",
1262
+ "previoustrack",
1263
+ "nexttrack",
1264
+ "seekto",
1265
+ "stop"
1266
+ ];
1267
+ for (const action of actions) {
1268
+ setActionHandler(action, null);
1269
+ }
1270
+ navigator.mediaSession.metadata = null;
1271
+ navigator.mediaSession.playbackState = "none";
1272
+ try {
1273
+ navigator.mediaSession.setPositionState();
1274
+ } catch {
1275
+ }
1276
+ }
1277
+ };
1278
+ };
1279
+ // Annotate the CommonJS export names for ESM import in node:
1280
+ 0 && (module.exports = {
1281
+ attachAudioPlayerMediaSession,
1282
+ attachAudioPlayerPersistence,
1283
+ attachAudioPlayerProgressReporter,
1284
+ createAudioPlayer,
1285
+ createLocalStoragePersistenceAdapter
1286
+ });
1287
+ //# sourceMappingURL=index.cjs.map