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