@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/react.cjs ADDED
@@ -0,0 +1,924 @@
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/react.tsx
21
+ var react_exports = {};
22
+ __export(react_exports, {
23
+ AudioPlayerProvider: () => AudioPlayerProvider,
24
+ useAudioPlayer: () => useAudioPlayer,
25
+ useAudioPlayerState: () => useAudioPlayerState
26
+ });
27
+ module.exports = __toCommonJS(react_exports);
28
+ var import_react = require("react");
29
+
30
+ // src/index.ts
31
+ var DEFAULT_PREVIOUS_RESTART_THRESHOLD_SECONDS = 5;
32
+ var DEFAULT_PERSISTENCE_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1e3;
33
+ var createInitialState = (options) => ({
34
+ status: "idle",
35
+ currentSource: null,
36
+ currentSourceId: null,
37
+ currentTime: 0,
38
+ duration: 0,
39
+ buffered: 0,
40
+ progress: {
41
+ currentTime: 0,
42
+ duration: 0,
43
+ buffered: 0,
44
+ playedFraction: 0,
45
+ bufferedFraction: 0
46
+ },
47
+ rate: options.initialRate ?? 1,
48
+ volume: options.initialVolume ?? 1,
49
+ muted: options.initialMuted ?? false,
50
+ queue: {
51
+ items: [],
52
+ itemIds: [],
53
+ position: {
54
+ currentIndex: -1,
55
+ currentSourceId: null,
56
+ length: 0,
57
+ repeatMode: options.initialRepeatMode ?? "off",
58
+ hasNext: false,
59
+ hasPrevious: false
60
+ }
61
+ },
62
+ error: null
63
+ });
64
+ var createPlayerError = (code, message, cause) => ({
65
+ code,
66
+ message,
67
+ cause
68
+ });
69
+ var isRuntimePlayerError = (value) => !!value && typeof value === "object" && "code" in value && "message" in value;
70
+ var normalizePlayerError = (fallbackCode, fallbackMessage, cause) => isRuntimePlayerError(cause) ? cause : createPlayerError(fallbackCode, fallbackMessage, cause);
71
+ var assertStableSource = (source) => {
72
+ if (!source.id || !source.src) {
73
+ throw createPlayerError(
74
+ "INVALID_QUEUE_ITEM",
75
+ "AudioSource requires stable id and src."
76
+ );
77
+ }
78
+ };
79
+ var isFiniteNumber = (value) => Number.isFinite(value) && !Number.isNaN(value);
80
+ var safeDuration = (audio, hint) => {
81
+ const duration = audio?.duration;
82
+ return duration !== void 0 && isFiniteNumber(duration) ? duration : hint ?? 0;
83
+ };
84
+ var safeBuffered = (audio) => {
85
+ if (!audio || audio.buffered.length === 0) {
86
+ return 0;
87
+ }
88
+ try {
89
+ return audio.buffered.end(audio.buffered.length - 1);
90
+ } catch {
91
+ return 0;
92
+ }
93
+ };
94
+ var cloneQueueState = (state) => ({
95
+ items: [...state.queue.items],
96
+ itemIds: [...state.queue.itemIds],
97
+ position: {
98
+ ...state.queue.position
99
+ }
100
+ });
101
+ var updateQueueFlags = (state) => {
102
+ const { items, position } = state.queue;
103
+ const { currentIndex, repeatMode } = position;
104
+ const lastIndex = items.length - 1;
105
+ const hasItems = items.length > 0;
106
+ const hasNext = !hasItems || currentIndex < 0 ? false : repeatMode === "all" ? items.length > 0 : currentIndex < lastIndex;
107
+ const hasPrevious = !hasItems || currentIndex < 0 ? false : repeatMode === "all" ? items.length > 0 : currentIndex > 0;
108
+ return {
109
+ ...state,
110
+ queue: {
111
+ ...state.queue,
112
+ itemIds: items.map((item) => item.id),
113
+ position: {
114
+ ...position,
115
+ currentSourceId: state.currentSource?.id ?? null,
116
+ length: items.length,
117
+ hasNext,
118
+ hasPrevious
119
+ }
120
+ }
121
+ };
122
+ };
123
+ var createAudioElement = () => {
124
+ if (typeof Audio === "undefined") {
125
+ return null;
126
+ }
127
+ return new Audio();
128
+ };
129
+ var createUnavailableAudioEngine = () => {
130
+ const unavailableError = () => createPlayerError(
131
+ "UNSUPPORTED_ENVIRONMENT",
132
+ "HTMLAudioElement is not available in this environment."
133
+ );
134
+ return {
135
+ available: false,
136
+ getSnapshot(durationHint) {
137
+ return {
138
+ currentTime: 0,
139
+ duration: durationHint ?? 0,
140
+ buffered: 0,
141
+ rate: 1,
142
+ volume: 1,
143
+ muted: false
144
+ };
145
+ },
146
+ getRuntimeError() {
147
+ return unavailableError();
148
+ },
149
+ on() {
150
+ return () => {
151
+ };
152
+ },
153
+ setSource() {
154
+ throw unavailableError();
155
+ },
156
+ clearSource() {
157
+ throw unavailableError();
158
+ },
159
+ load() {
160
+ throw unavailableError();
161
+ },
162
+ play() {
163
+ throw unavailableError();
164
+ },
165
+ pause() {
166
+ throw unavailableError();
167
+ },
168
+ setCurrentTime() {
169
+ throw unavailableError();
170
+ },
171
+ setPlaybackRate() {
172
+ throw unavailableError();
173
+ },
174
+ setVolume() {
175
+ throw unavailableError();
176
+ },
177
+ setMuted() {
178
+ throw unavailableError();
179
+ },
180
+ destroy() {
181
+ }
182
+ };
183
+ };
184
+ var createHtmlAudioEngine = () => {
185
+ const audio = createAudioElement();
186
+ if (!audio) {
187
+ return createUnavailableAudioEngine();
188
+ }
189
+ audio.preload = "metadata";
190
+ const clearSource = () => {
191
+ const element = audio;
192
+ if (typeof element.removeAttribute === "function") {
193
+ element.removeAttribute("src");
194
+ return;
195
+ }
196
+ audio.src = "";
197
+ };
198
+ return {
199
+ available: true,
200
+ getSnapshot(durationHint) {
201
+ return {
202
+ currentTime: audio.currentTime,
203
+ duration: safeDuration(audio, durationHint),
204
+ buffered: safeBuffered(audio),
205
+ rate: audio.playbackRate,
206
+ volume: audio.volume,
207
+ muted: audio.muted
208
+ };
209
+ },
210
+ getRuntimeError() {
211
+ return audio.error;
212
+ },
213
+ on(eventName, listener) {
214
+ audio.addEventListener(eventName, listener);
215
+ return () => {
216
+ audio.removeEventListener(eventName, listener);
217
+ };
218
+ },
219
+ setSource(src) {
220
+ audio.src = src;
221
+ },
222
+ clearSource() {
223
+ clearSource();
224
+ },
225
+ load() {
226
+ audio.load();
227
+ },
228
+ play() {
229
+ return audio.play();
230
+ },
231
+ pause() {
232
+ audio.pause();
233
+ },
234
+ setCurrentTime(time) {
235
+ audio.currentTime = time;
236
+ },
237
+ setPlaybackRate(rate) {
238
+ audio.playbackRate = rate;
239
+ },
240
+ setVolume(volume) {
241
+ audio.volume = volume;
242
+ },
243
+ setMuted(muted) {
244
+ audio.muted = muted;
245
+ },
246
+ destroy() {
247
+ audio.pause();
248
+ clearSource();
249
+ audio.load();
250
+ }
251
+ };
252
+ };
253
+ var createAudioPlayer = (options = {}) => {
254
+ let state = updateQueueFlags(createInitialState(options));
255
+ let destroyed = false;
256
+ let shouldAutoplayAfterLoad = false;
257
+ let suppressPauseEvent = false;
258
+ const engine = createHtmlAudioEngine();
259
+ const subscribers = /* @__PURE__ */ new Set();
260
+ const eventListeners = {
261
+ sourcechange: /* @__PURE__ */ new Set(),
262
+ queuechange: /* @__PURE__ */ new Set(),
263
+ play: /* @__PURE__ */ new Set(),
264
+ pause: /* @__PURE__ */ new Set(),
265
+ seeking: /* @__PURE__ */ new Set(),
266
+ seeked: /* @__PURE__ */ new Set(),
267
+ timeupdate: /* @__PURE__ */ new Set(),
268
+ ratechange: /* @__PURE__ */ new Set(),
269
+ volumechange: /* @__PURE__ */ new Set(),
270
+ ended: /* @__PURE__ */ new Set(),
271
+ error: /* @__PURE__ */ new Set(),
272
+ destroy: /* @__PURE__ */ new Set()
273
+ };
274
+ const getState = () => state;
275
+ const publishState = () => {
276
+ for (const subscriber of subscribers) {
277
+ subscriber(state);
278
+ }
279
+ };
280
+ const emit = (eventName, event) => {
281
+ for (const listener of eventListeners[eventName]) {
282
+ listener(event);
283
+ }
284
+ };
285
+ const setState = (updater) => {
286
+ state = updateQueueFlags(updater(state));
287
+ publishState();
288
+ return state;
289
+ };
290
+ const setError = (error) => {
291
+ shouldAutoplayAfterLoad = false;
292
+ const nextState = setState((currentState) => ({
293
+ ...currentState,
294
+ status: "error",
295
+ error: {
296
+ code: error.code,
297
+ message: error.message
298
+ }
299
+ }));
300
+ emit("error", { error, state: nextState });
301
+ };
302
+ const clearError = () => {
303
+ if (!state.error) {
304
+ return;
305
+ }
306
+ setState((currentState) => ({
307
+ ...currentState,
308
+ error: null
309
+ }));
310
+ };
311
+ const syncAudioSnapshot = (statusOverride) => {
312
+ const snapshot = engine.getSnapshot(state.currentSource?.durationHint);
313
+ const currentTime = snapshot.currentTime;
314
+ const duration = snapshot.duration;
315
+ const buffered = snapshot.buffered;
316
+ const playedFraction = duration > 0 ? Math.min(1, Math.max(0, currentTime / duration)) : 0;
317
+ const bufferedFraction = duration > 0 ? Math.min(1, Math.max(0, buffered / duration)) : 0;
318
+ const nextState = setState((currentState) => ({
319
+ ...currentState,
320
+ currentSourceId: currentState.currentSource?.id ?? null,
321
+ currentTime,
322
+ duration,
323
+ buffered,
324
+ progress: {
325
+ currentTime,
326
+ duration,
327
+ buffered,
328
+ playedFraction,
329
+ bufferedFraction
330
+ },
331
+ rate: snapshot.rate,
332
+ volume: snapshot.volume,
333
+ muted: snapshot.muted,
334
+ status: statusOverride ?? currentState.status,
335
+ error: statusOverride === "error" ? currentState.error : null
336
+ }));
337
+ return nextState;
338
+ };
339
+ const findQueueIndexById = (id) => id ? state.queue.items.findIndex((item) => item.id === id) : -1;
340
+ const resolveNextQueueIndex = (mode) => {
341
+ const { items, position } = state.queue;
342
+ const { currentIndex, repeatMode } = position;
343
+ if (items.length === 0 || currentIndex < 0) {
344
+ return null;
345
+ }
346
+ if (repeatMode === "one") {
347
+ return currentIndex;
348
+ }
349
+ if (currentIndex < items.length - 1) {
350
+ return currentIndex + 1;
351
+ }
352
+ if (repeatMode === "all") {
353
+ return 0;
354
+ }
355
+ return mode === "ended" ? -1 : null;
356
+ };
357
+ const resolvePreviousQueueIndex = () => {
358
+ const { items, position } = state.queue;
359
+ const { currentIndex, repeatMode } = position;
360
+ if (items.length === 0 || currentIndex < 0) {
361
+ return null;
362
+ }
363
+ if (currentIndex > 0) {
364
+ return currentIndex - 1;
365
+ }
366
+ if (repeatMode === "all") {
367
+ return items.length - 1;
368
+ }
369
+ return null;
370
+ };
371
+ const updateCurrentSource = (source, nextStatus) => {
372
+ const queueIndex = findQueueIndexById(source?.id);
373
+ const nextState = setState((currentState) => ({
374
+ ...currentState,
375
+ currentSource: source,
376
+ currentSourceId: source?.id ?? null,
377
+ currentTime: 0,
378
+ duration: source?.durationHint ?? 0,
379
+ buffered: 0,
380
+ progress: {
381
+ currentTime: 0,
382
+ duration: source?.durationHint ?? 0,
383
+ buffered: 0,
384
+ playedFraction: 0,
385
+ bufferedFraction: 0
386
+ },
387
+ status: nextStatus,
388
+ error: null,
389
+ queue: {
390
+ ...currentState.queue,
391
+ position: {
392
+ ...currentState.queue.position,
393
+ currentIndex: queueIndex,
394
+ currentSourceId: source?.id ?? null
395
+ }
396
+ }
397
+ }));
398
+ emit("sourcechange", { source, state: nextState });
399
+ return nextState;
400
+ };
401
+ const setQueueState = (items, currentIndex) => setState((currentState) => ({
402
+ ...currentState,
403
+ queue: {
404
+ ...currentState.queue,
405
+ items,
406
+ position: {
407
+ ...currentState.queue.position,
408
+ currentIndex
409
+ }
410
+ }
411
+ }));
412
+ const emitQueueChange = (nextState) => {
413
+ emit("queuechange", { queue: cloneQueueState(nextState), state: nextState });
414
+ };
415
+ const attachSourceToAudio = (source, autoplayAfterLoad) => {
416
+ shouldAutoplayAfterLoad = autoplayAfterLoad;
417
+ suppressPauseEvent = true;
418
+ try {
419
+ engine.pause();
420
+ } finally {
421
+ suppressPauseEvent = false;
422
+ }
423
+ engine.setSource(source.src);
424
+ engine.setCurrentTime(0);
425
+ engine.setPlaybackRate(state.rate);
426
+ engine.setVolume(state.volume);
427
+ engine.setMuted(state.muted);
428
+ engine.load();
429
+ };
430
+ const loadInternal = async (source, autoplayAfterLoad = false) => {
431
+ assertStableSource(source);
432
+ updateCurrentSource(source, "loading");
433
+ attachSourceToAudio(source, autoplayAfterLoad);
434
+ };
435
+ const selectQueueIndex = async (index, autoplayAfterLoad = false) => {
436
+ const item = state.queue.items[index] ?? null;
437
+ if (!item) {
438
+ return null;
439
+ }
440
+ await loadInternal(item, autoplayAfterLoad);
441
+ const nextState = setQueueState(state.queue.items, index);
442
+ emitQueueChange(nextState);
443
+ return item;
444
+ };
445
+ const playLoadedAudio = async () => {
446
+ try {
447
+ await engine.play();
448
+ } catch (cause) {
449
+ setError(createPlayerError("PLAY_ERROR", "Failed to start playback.", cause));
450
+ }
451
+ };
452
+ const handlePlaybackEnded = async () => {
453
+ if (destroyed) {
454
+ return;
455
+ }
456
+ if (state.queue.items.length > 0 && state.queue.position.currentIndex >= 0) {
457
+ const nextIndex = resolveNextQueueIndex("ended");
458
+ if (nextIndex === -1 || nextIndex === null) {
459
+ const endedState2 = syncAudioSnapshot("ended");
460
+ emit("ended", { source: endedState2.currentSource, state: endedState2 });
461
+ return;
462
+ }
463
+ await selectQueueIndex(nextIndex, true);
464
+ return;
465
+ }
466
+ const endedState = syncAudioSnapshot("ended");
467
+ emit("ended", { source: endedState.currentSource, state: endedState });
468
+ };
469
+ const detachEngineListeners = [
470
+ engine.on("loadedmetadata", () => {
471
+ if (destroyed || !state.currentSource) {
472
+ return;
473
+ }
474
+ syncAudioSnapshot("ready");
475
+ if (shouldAutoplayAfterLoad) {
476
+ shouldAutoplayAfterLoad = false;
477
+ void playLoadedAudio();
478
+ }
479
+ }),
480
+ engine.on("timeupdate", () => {
481
+ if (destroyed) {
482
+ return;
483
+ }
484
+ const nextState = syncAudioSnapshot(
485
+ state.status === "ended" ? "playing" : void 0
486
+ );
487
+ emit("timeupdate", {
488
+ currentTime: nextState.currentTime,
489
+ duration: nextState.duration,
490
+ state: nextState
491
+ });
492
+ }),
493
+ engine.on("progress", () => {
494
+ if (!destroyed) {
495
+ syncAudioSnapshot();
496
+ }
497
+ }),
498
+ engine.on("play", () => {
499
+ if (destroyed) {
500
+ return;
501
+ }
502
+ const nextState = syncAudioSnapshot("playing");
503
+ emit("play", { source: nextState.currentSource, state: nextState });
504
+ }),
505
+ engine.on("pause", () => {
506
+ if (destroyed || suppressPauseEvent) {
507
+ return;
508
+ }
509
+ const nextStatus = state.status === "ended" ? "ended" : state.currentSource ? "paused" : "idle";
510
+ const nextState = syncAudioSnapshot(nextStatus);
511
+ emit("pause", { source: nextState.currentSource, state: nextState });
512
+ }),
513
+ engine.on("seeked", () => {
514
+ if (destroyed) {
515
+ return;
516
+ }
517
+ const nextState = syncAudioSnapshot();
518
+ emit("seeked", { currentTime: nextState.currentTime, state: nextState });
519
+ emit("timeupdate", {
520
+ currentTime: nextState.currentTime,
521
+ duration: nextState.duration,
522
+ state: nextState
523
+ });
524
+ }),
525
+ engine.on("ratechange", () => {
526
+ if (destroyed) {
527
+ return;
528
+ }
529
+ const nextState = syncAudioSnapshot();
530
+ emit("ratechange", { rate: nextState.rate, state: nextState });
531
+ }),
532
+ engine.on("volumechange", () => {
533
+ if (destroyed) {
534
+ return;
535
+ }
536
+ const nextState = syncAudioSnapshot();
537
+ emit("volumechange", {
538
+ volume: nextState.volume,
539
+ muted: nextState.muted,
540
+ state: nextState
541
+ });
542
+ }),
543
+ engine.on("ended", () => {
544
+ void handlePlaybackEnded();
545
+ }),
546
+ engine.on("error", () => {
547
+ if (destroyed) {
548
+ return;
549
+ }
550
+ setError(
551
+ createPlayerError(
552
+ "LOAD_ERROR",
553
+ "Audio element reported a playback error.",
554
+ engine.getRuntimeError()
555
+ )
556
+ );
557
+ })
558
+ ];
559
+ return {
560
+ getState,
561
+ subscribe(listener) {
562
+ subscribers.add(listener);
563
+ listener(state);
564
+ return () => {
565
+ subscribers.delete(listener);
566
+ };
567
+ },
568
+ on(eventName, listener) {
569
+ eventListeners[eventName].add(
570
+ listener
571
+ );
572
+ return () => {
573
+ eventListeners[eventName].delete(
574
+ listener
575
+ );
576
+ };
577
+ },
578
+ async load(source) {
579
+ try {
580
+ await loadInternal(source);
581
+ } catch (cause) {
582
+ setError(
583
+ normalizePlayerError("LOAD_ERROR", "Failed to load source.", cause)
584
+ );
585
+ }
586
+ },
587
+ async play(source) {
588
+ try {
589
+ if (source) {
590
+ await loadInternal(source, true);
591
+ return;
592
+ }
593
+ if (!state.currentSource) {
594
+ setError(
595
+ createPlayerError(
596
+ "NO_ACTIVE_SOURCE",
597
+ "Cannot play without an active source."
598
+ )
599
+ );
600
+ return;
601
+ }
602
+ if (state.status === "loading") {
603
+ shouldAutoplayAfterLoad = true;
604
+ return;
605
+ }
606
+ await playLoadedAudio();
607
+ } catch (cause) {
608
+ setError(normalizePlayerError("PLAY_ERROR", "Failed to start playback.", cause));
609
+ }
610
+ },
611
+ pause() {
612
+ if (!engine.available) {
613
+ return;
614
+ }
615
+ shouldAutoplayAfterLoad = false;
616
+ engine.pause();
617
+ },
618
+ async toggle() {
619
+ if (state.status === "playing") {
620
+ this.pause();
621
+ return;
622
+ }
623
+ await this.play();
624
+ },
625
+ unload() {
626
+ shouldAutoplayAfterLoad = false;
627
+ if (engine.available) {
628
+ suppressPauseEvent = true;
629
+ try {
630
+ engine.pause();
631
+ } finally {
632
+ suppressPauseEvent = false;
633
+ }
634
+ engine.clearSource();
635
+ engine.load();
636
+ }
637
+ const nextState = updateCurrentSource(null, "idle");
638
+ emitQueueChange(nextState);
639
+ },
640
+ seek(time) {
641
+ if (!state.currentSource) {
642
+ setError(
643
+ createPlayerError("SEEK_ERROR", "Cannot seek without an active source.")
644
+ );
645
+ return;
646
+ }
647
+ const max = engine.getSnapshot(state.currentSource.durationHint).duration;
648
+ const normalized = Math.max(
649
+ 0,
650
+ max > 0 ? Math.min(time, max) : isFiniteNumber(time) ? time : 0
651
+ );
652
+ emit("seeking", { from: state.currentTime, to: normalized, state });
653
+ try {
654
+ engine.setCurrentTime(normalized);
655
+ } catch (cause) {
656
+ setError(normalizePlayerError("SEEK_ERROR", "Failed to seek audio.", cause));
657
+ }
658
+ },
659
+ setRate(rate) {
660
+ if (!engine.available) {
661
+ setError(
662
+ createPlayerError("INVALID_RATE", "Audio element is unavailable.")
663
+ );
664
+ return;
665
+ }
666
+ if (!isFiniteNumber(rate) || rate <= 0) {
667
+ setError(
668
+ createPlayerError("INVALID_RATE", "Playback rate must be greater than 0.")
669
+ );
670
+ return;
671
+ }
672
+ engine.setPlaybackRate(rate);
673
+ clearError();
674
+ syncAudioSnapshot();
675
+ },
676
+ setVolume(volume) {
677
+ if (!engine.available) {
678
+ setError(
679
+ createPlayerError("INVALID_VOLUME", "Audio element is unavailable.")
680
+ );
681
+ return;
682
+ }
683
+ if (!isFiniteNumber(volume) || volume < 0 || volume > 1) {
684
+ setError(
685
+ createPlayerError("INVALID_VOLUME", "Volume must be between 0 and 1.")
686
+ );
687
+ return;
688
+ }
689
+ engine.setVolume(volume);
690
+ clearError();
691
+ syncAudioSnapshot();
692
+ },
693
+ setMuted(muted) {
694
+ if (!engine.available) {
695
+ return;
696
+ }
697
+ engine.setMuted(muted);
698
+ clearError();
699
+ syncAudioSnapshot();
700
+ },
701
+ async setQueue(items, queueOptions = {}) {
702
+ try {
703
+ for (const item of items) {
704
+ assertStableSource(item);
705
+ }
706
+ const startIndex = queueOptions.startAtId === void 0 ? items.length > 0 ? 0 : -1 : items.findIndex((item) => item.id === queueOptions.startAtId);
707
+ if (queueOptions.startAtId && startIndex < 0) {
708
+ throw createPlayerError(
709
+ "INVALID_QUEUE_ITEM",
710
+ `Queue start item ${queueOptions.startAtId} was not found.`
711
+ );
712
+ }
713
+ const nextState = setState((currentState) => ({
714
+ ...currentState,
715
+ currentSource: startIndex >= 0 ? items[startIndex] ?? null : null,
716
+ currentSourceId: startIndex >= 0 ? items[startIndex]?.id ?? null : null,
717
+ currentTime: 0,
718
+ duration: startIndex >= 0 ? items[startIndex]?.durationHint ?? 0 : 0,
719
+ buffered: 0,
720
+ progress: {
721
+ currentTime: 0,
722
+ duration: startIndex >= 0 ? items[startIndex]?.durationHint ?? 0 : 0,
723
+ buffered: 0,
724
+ playedFraction: 0,
725
+ bufferedFraction: 0
726
+ },
727
+ status: startIndex >= 0 ? "ready" : "idle",
728
+ error: null,
729
+ queue: {
730
+ ...currentState.queue,
731
+ items: [...items],
732
+ position: {
733
+ ...currentState.queue.position,
734
+ currentIndex: startIndex,
735
+ currentSourceId: startIndex >= 0 ? items[startIndex]?.id ?? null : null
736
+ }
737
+ }
738
+ }));
739
+ emitQueueChange(nextState);
740
+ emit("sourcechange", {
741
+ source: nextState.currentSource,
742
+ state: nextState
743
+ });
744
+ if (nextState.currentSource) {
745
+ await loadInternal(nextState.currentSource, queueOptions.autoplay === true);
746
+ const syncedState = setQueueState(items, startIndex);
747
+ emitQueueChange(syncedState);
748
+ } else if (engine.available) {
749
+ shouldAutoplayAfterLoad = false;
750
+ suppressPauseEvent = true;
751
+ engine.pause();
752
+ suppressPauseEvent = false;
753
+ engine.clearSource();
754
+ engine.load();
755
+ }
756
+ } catch (cause) {
757
+ setError(
758
+ normalizePlayerError(
759
+ "INVALID_QUEUE_ITEM",
760
+ "Failed to set queue.",
761
+ cause
762
+ )
763
+ );
764
+ }
765
+ },
766
+ appendToQueue(items) {
767
+ try {
768
+ for (const item of items) {
769
+ assertStableSource(item);
770
+ }
771
+ const nextState = setState((currentState) => ({
772
+ ...currentState,
773
+ queue: {
774
+ ...currentState.queue,
775
+ items: [...currentState.queue.items, ...items]
776
+ }
777
+ }));
778
+ emitQueueChange(nextState);
779
+ } catch (cause) {
780
+ setError(
781
+ normalizePlayerError(
782
+ "INVALID_QUEUE_ITEM",
783
+ "Failed to append queue items.",
784
+ cause
785
+ )
786
+ );
787
+ }
788
+ },
789
+ removeFromQueue(id) {
790
+ const nextItems = state.queue.items.filter((item) => item.id !== id);
791
+ const nextIndex = nextItems.findIndex(
792
+ (item) => item.id === state.currentSource?.id
793
+ );
794
+ const nextState = setState((currentState) => ({
795
+ ...currentState,
796
+ queue: {
797
+ ...currentState.queue,
798
+ items: nextItems,
799
+ position: {
800
+ ...currentState.queue.position,
801
+ currentIndex: nextIndex,
802
+ currentSourceId: nextIndex >= 0 ? nextItems[nextIndex]?.id ?? null : null
803
+ }
804
+ }
805
+ }));
806
+ emitQueueChange(nextState);
807
+ },
808
+ clearQueue() {
809
+ const nextState = setState((currentState) => ({
810
+ ...currentState,
811
+ queue: {
812
+ ...currentState.queue,
813
+ items: [],
814
+ position: {
815
+ ...currentState.queue.position,
816
+ currentIndex: -1,
817
+ currentSourceId: null
818
+ }
819
+ }
820
+ }));
821
+ emitQueueChange(nextState);
822
+ },
823
+ async next() {
824
+ const nextIndex = resolveNextQueueIndex("manual");
825
+ if (nextIndex === null) {
826
+ return;
827
+ }
828
+ await selectQueueIndex(nextIndex, true);
829
+ },
830
+ async previous(previousOptions = {}) {
831
+ if (!state.currentSource) {
832
+ return;
833
+ }
834
+ const threshold = previousOptions.restartThresholdSeconds ?? DEFAULT_PREVIOUS_RESTART_THRESHOLD_SECONDS;
835
+ if (state.currentTime > threshold) {
836
+ this.seek(0);
837
+ return;
838
+ }
839
+ const previousIndex = resolvePreviousQueueIndex();
840
+ if (previousIndex === null) {
841
+ this.seek(0);
842
+ return;
843
+ }
844
+ await selectQueueIndex(previousIndex, true);
845
+ },
846
+ setRepeatMode(mode) {
847
+ const nextState = setState((currentState) => ({
848
+ ...currentState,
849
+ queue: {
850
+ ...currentState.queue,
851
+ position: {
852
+ ...currentState.queue.position,
853
+ repeatMode: mode
854
+ }
855
+ }
856
+ }));
857
+ emitQueueChange(nextState);
858
+ },
859
+ destroy() {
860
+ if (destroyed) {
861
+ return;
862
+ }
863
+ destroyed = true;
864
+ shouldAutoplayAfterLoad = false;
865
+ for (const detach of detachEngineListeners) {
866
+ detach();
867
+ }
868
+ suppressPauseEvent = true;
869
+ engine.destroy();
870
+ suppressPauseEvent = false;
871
+ const nextState = setState(() => createInitialState(options));
872
+ emit("destroy", { state: nextState });
873
+ subscribers.clear();
874
+ for (const eventName of Object.keys(eventListeners)) {
875
+ eventListeners[eventName].clear();
876
+ }
877
+ }
878
+ };
879
+ };
880
+
881
+ // src/react.tsx
882
+ var import_jsx_runtime = require("react/jsx-runtime");
883
+ var AudioPlayerContext = (0, import_react.createContext)(null);
884
+ var AudioPlayerProvider = ({
885
+ children,
886
+ options,
887
+ player
888
+ }) => {
889
+ const playerRef = (0, import_react.useRef)(player ?? null);
890
+ if (playerRef.current === null) {
891
+ playerRef.current = createAudioPlayer(options);
892
+ }
893
+ (0, import_react.useEffect)(() => {
894
+ if (!player) {
895
+ return () => {
896
+ playerRef.current?.destroy();
897
+ };
898
+ }
899
+ return void 0;
900
+ }, [player]);
901
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AudioPlayerContext.Provider, { value: playerRef.current, children });
902
+ };
903
+ var useAudioPlayer = () => {
904
+ const player = (0, import_react.useContext)(AudioPlayerContext);
905
+ if (!player) {
906
+ throw new Error("useAudioPlayer must be used inside AudioPlayerProvider.");
907
+ }
908
+ return player;
909
+ };
910
+ var useAudioPlayerState = () => {
911
+ const player = useAudioPlayer();
912
+ return (0, import_react.useSyncExternalStore)(
913
+ player.subscribe,
914
+ player.getState,
915
+ player.getState
916
+ );
917
+ };
918
+ // Annotate the CommonJS export names for ESM import in node:
919
+ 0 && (module.exports = {
920
+ AudioPlayerProvider,
921
+ useAudioPlayer,
922
+ useAudioPlayerState
923
+ });
924
+ //# sourceMappingURL=react.cjs.map