@gentomiyano/optimized-web-audio-player 0.1.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.
Files changed (53) hide show
  1. package/README.md +159 -0
  2. package/dist/config/index.d.ts +37 -0
  3. package/dist/config/index.js +280 -0
  4. package/dist/config/index.js.map +1 -0
  5. package/dist/consumer/index.d.ts +27 -0
  6. package/dist/consumer/index.js +38 -0
  7. package/dist/consumer/index.js.map +1 -0
  8. package/dist/consumer-smoke/node_modules/.modules.yaml +30 -0
  9. package/dist/consumer-smoke/node_modules/.package-map.json +1 -0
  10. package/dist/consumer-smoke/node_modules/.pnpm/lock.yaml +57 -0
  11. package/dist/consumer-smoke/node_modules/.pnpm/scheduler@0.27.0/node_modules/scheduler/LICENSE +21 -0
  12. package/dist/consumer-smoke/node_modules/.pnpm/scheduler@0.27.0/node_modules/scheduler/README.md +9 -0
  13. package/dist/consumer-smoke/node_modules/.pnpm/scheduler@0.27.0/node_modules/scheduler/cjs/scheduler-unstable_mock.development.js +414 -0
  14. package/dist/consumer-smoke/node_modules/.pnpm/scheduler@0.27.0/node_modules/scheduler/cjs/scheduler-unstable_mock.production.js +406 -0
  15. package/dist/consumer-smoke/node_modules/.pnpm/scheduler@0.27.0/node_modules/scheduler/cjs/scheduler-unstable_post_task.development.js +150 -0
  16. package/dist/consumer-smoke/node_modules/.pnpm/scheduler@0.27.0/node_modules/scheduler/cjs/scheduler-unstable_post_task.production.js +140 -0
  17. package/dist/consumer-smoke/node_modules/.pnpm/scheduler@0.27.0/node_modules/scheduler/cjs/scheduler.development.js +364 -0
  18. package/dist/consumer-smoke/node_modules/.pnpm/scheduler@0.27.0/node_modules/scheduler/cjs/scheduler.native.development.js +350 -0
  19. package/dist/consumer-smoke/node_modules/.pnpm/scheduler@0.27.0/node_modules/scheduler/cjs/scheduler.native.production.js +330 -0
  20. package/dist/consumer-smoke/node_modules/.pnpm/scheduler@0.27.0/node_modules/scheduler/cjs/scheduler.production.js +340 -0
  21. package/dist/consumer-smoke/node_modules/.pnpm/scheduler@0.27.0/node_modules/scheduler/index.js +7 -0
  22. package/dist/consumer-smoke/node_modules/.pnpm/scheduler@0.27.0/node_modules/scheduler/index.native.js +7 -0
  23. package/dist/consumer-smoke/node_modules/.pnpm/scheduler@0.27.0/node_modules/scheduler/package.json +27 -0
  24. package/dist/consumer-smoke/node_modules/.pnpm/scheduler@0.27.0/node_modules/scheduler/unstable_mock.js +7 -0
  25. package/dist/consumer-smoke/node_modules/.pnpm/scheduler@0.27.0/node_modules/scheduler/unstable_post_task.js +7 -0
  26. package/dist/consumer-smoke/node_modules/.pnpm-workspace-state-v1.json +29 -0
  27. package/dist/contracts/index.d.ts +15 -0
  28. package/dist/contracts/index.js +3 -0
  29. package/dist/contracts/index.js.map +1 -0
  30. package/dist/index.d.ts +6 -0
  31. package/dist/index.js +368 -0
  32. package/dist/index.js.map +1 -0
  33. package/dist/media-delivery/index.d.ts +185 -0
  34. package/dist/media-delivery/index.js +91 -0
  35. package/dist/media-delivery/index.js.map +1 -0
  36. package/dist/media-element-C9dis4aG.d.ts +40 -0
  37. package/dist/player-DE9Vqztu.d.ts +212 -0
  38. package/dist/ports/index.d.ts +32 -0
  39. package/dist/ports/index.js +3 -0
  40. package/dist/ports/index.js.map +1 -0
  41. package/dist/react/index.d.ts +86 -0
  42. package/dist/react/index.js +2393 -0
  43. package/dist/react/index.js.map +1 -0
  44. package/dist/testing/index.d.ts +36 -0
  45. package/dist/testing/index.js +365 -0
  46. package/dist/testing/index.js.map +1 -0
  47. package/dist/ui/index.d.ts +143 -0
  48. package/dist/ui/index.js +1344 -0
  49. package/dist/ui/index.js.map +1 -0
  50. package/dist/ui/player-tokens.css +572 -0
  51. package/dist/ui/player-views.css +654 -0
  52. package/dist/ui/styles.css +2 -0
  53. package/package.json +97 -0
@@ -0,0 +1,2393 @@
1
+ import { createContext, useRef, useState, useEffect, useSyncExternalStore, useContext } from 'react';
2
+ import { jsxs, Fragment, jsx } from 'react/jsx-runtime';
3
+
4
+ // src/react/player-provider.tsx
5
+
6
+ // src/adapters/browser/browser-media-element-port.ts
7
+ function clampVolume(value) {
8
+ if (!Number.isFinite(value)) {
9
+ return 0;
10
+ }
11
+ return Math.min(Math.max(value, 0), 1);
12
+ }
13
+ function classifyMediaError(mediaError) {
14
+ if (mediaError === null) {
15
+ return null;
16
+ }
17
+ switch (mediaError.code) {
18
+ case MediaError.MEDIA_ERR_NETWORK:
19
+ return {
20
+ code: "network",
21
+ recoverable: true,
22
+ messageKey: "player.error.network"
23
+ };
24
+ case MediaError.MEDIA_ERR_DECODE:
25
+ return {
26
+ code: "decode",
27
+ recoverable: false,
28
+ messageKey: "player.error.decode"
29
+ };
30
+ case MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED:
31
+ return {
32
+ code: "unsupported",
33
+ recoverable: false,
34
+ messageKey: "player.error.unsupported"
35
+ };
36
+ default:
37
+ return {
38
+ code: "unknown",
39
+ recoverable: true,
40
+ messageKey: "player.error.unknown"
41
+ };
42
+ }
43
+ }
44
+ var BrowserMediaElementPort = class {
45
+ #element;
46
+ #listeners = /* @__PURE__ */ new Map();
47
+ #assignedSourceUrl = null;
48
+ constructor(element) {
49
+ this.#element = element;
50
+ }
51
+ getCurrentTimeSec() {
52
+ return Number.isFinite(this.#element.currentTime) ? Math.max(this.#element.currentTime, 0) : 0;
53
+ }
54
+ getDurationSec() {
55
+ return Number.isFinite(this.#element.duration) && this.#element.duration > 0 ? this.#element.duration : null;
56
+ }
57
+ getVolume() {
58
+ return clampVolume(this.#element.volume);
59
+ }
60
+ isMuted() {
61
+ return this.#element.muted;
62
+ }
63
+ isPaused() {
64
+ return this.#element.paused;
65
+ }
66
+ getStateSnapshot() {
67
+ const currentSource = this.#element.currentSrc || this.#element.getAttribute("src") || "";
68
+ const baseUrl = this.#element.baseURI;
69
+ let normalizedCurrentSource = currentSource;
70
+ if (currentSource.length > 0) {
71
+ try {
72
+ normalizedCurrentSource = new URL(
73
+ currentSource,
74
+ baseUrl
75
+ ).href;
76
+ } catch {
77
+ }
78
+ }
79
+ return Object.freeze({
80
+ currentTimeSec: this.getCurrentTimeSec(),
81
+ durationSec: this.getDurationSec(),
82
+ volume: this.getVolume(),
83
+ muted: this.isMuted(),
84
+ paused: this.isPaused(),
85
+ ended: this.#element.ended,
86
+ hasCurrentSource: currentSource.length > 0,
87
+ currentSourceMatchesAssigned: this.#assignedSourceUrl !== null && normalizedCurrentSource === this.#assignedSourceUrl
88
+ });
89
+ }
90
+ getError() {
91
+ return classifyMediaError(this.#element.error);
92
+ }
93
+ setSource(source) {
94
+ this.#element.pause();
95
+ this.#element.crossOrigin = source.crossOrigin;
96
+ this.#element.src = source.url;
97
+ this.#assignedSourceUrl = this.#element.src;
98
+ }
99
+ clearSource() {
100
+ this.#element.pause();
101
+ this.#assignedSourceUrl = null;
102
+ this.#element.removeAttribute("src");
103
+ this.#element.load();
104
+ }
105
+ load() {
106
+ this.#element.load();
107
+ }
108
+ async play() {
109
+ await this.#element.play();
110
+ }
111
+ pause() {
112
+ this.#element.pause();
113
+ }
114
+ seekTo(seconds) {
115
+ if (!Number.isFinite(seconds)) {
116
+ return;
117
+ }
118
+ this.#element.currentTime = Math.max(seconds, 0);
119
+ }
120
+ setVolume(volume) {
121
+ this.#element.volume = clampVolume(volume);
122
+ }
123
+ setMuted(muted) {
124
+ this.#element.muted = muted;
125
+ }
126
+ addEventListener(type, listener) {
127
+ const typeListeners = this.#listeners.get(type) ?? /* @__PURE__ */ new Map();
128
+ if (typeListeners.has(listener)) {
129
+ return;
130
+ }
131
+ const nativeListener = () => {
132
+ listener({ type });
133
+ };
134
+ typeListeners.set(listener, nativeListener);
135
+ this.#listeners.set(type, typeListeners);
136
+ this.#element.addEventListener(type, nativeListener);
137
+ }
138
+ removeEventListener(type, listener) {
139
+ const typeListeners = this.#listeners.get(type);
140
+ const nativeListener = typeListeners?.get(listener);
141
+ if (nativeListener === void 0) {
142
+ return;
143
+ }
144
+ this.#element.removeEventListener(type, nativeListener);
145
+ typeListeners?.delete(listener);
146
+ if (typeListeners?.size === 0) {
147
+ this.#listeners.delete(type);
148
+ }
149
+ }
150
+ };
151
+
152
+ // src/adapters/browser/audio-playback-mode.ts
153
+ function isIOSWebPlatform(platform) {
154
+ if (/\b(iPhone|iPad|iPod)\b/i.test(platform.userAgent)) {
155
+ return true;
156
+ }
157
+ return platform.platform === "MacIntel" && (platform.maxTouchPoints ?? 0) > 1;
158
+ }
159
+ function resolvePlayerAudioMode(requested, platform) {
160
+ if (requested !== "auto") {
161
+ return requested;
162
+ }
163
+ return isIOSWebPlatform(platform) ? "html-media" : "web-audio";
164
+ }
165
+ var HtmlMediaElementGainPort = class {
166
+ activateFromUserGesture() {
167
+ return Promise.resolve();
168
+ }
169
+ resumeIfSuspended() {
170
+ return Promise.resolve(false);
171
+ }
172
+ getValue() {
173
+ return 1;
174
+ }
175
+ setValue() {
176
+ }
177
+ cancelTransition() {
178
+ }
179
+ transitionTo() {
180
+ return Promise.resolve();
181
+ }
182
+ destroy() {
183
+ return Promise.resolve();
184
+ }
185
+ };
186
+
187
+ // src/adapters/browser/lazy-web-audio-gain-port.ts
188
+ var MIN_TRANSITION_SETTLE_MS = 32;
189
+ function clampGain(value) {
190
+ if (!Number.isFinite(value)) {
191
+ return 0;
192
+ }
193
+ return Math.min(Math.max(value, 0), 1);
194
+ }
195
+ function easingProgress(curve, progress, isFadeIn) {
196
+ switch (curve) {
197
+ case "ease-in":
198
+ return progress * progress;
199
+ case "ease-out":
200
+ return 1 - (1 - progress) ** 2;
201
+ case "equal-power":
202
+ return isFadeIn ? Math.sin(progress * Math.PI / 2) : 1 - Math.cos(progress * Math.PI / 2);
203
+ case "linear":
204
+ return progress;
205
+ }
206
+ }
207
+ function createCurve(start, target, curve) {
208
+ const values = new Float32Array(64);
209
+ const isFadeIn = target >= start;
210
+ for (let index = 0; index < values.length; index += 1) {
211
+ const progress = index / (values.length - 1);
212
+ const eased = easingProgress(curve, progress, isFadeIn);
213
+ values[index] = start + (target - start) * eased;
214
+ }
215
+ return values;
216
+ }
217
+ function transitionSettleMs(context) {
218
+ const contextWithLatency = context;
219
+ const baseLatency = Number.isFinite(contextWithLatency.baseLatency) ? Math.max(0, contextWithLatency.baseLatency) : 0;
220
+ const outputLatency = Number.isFinite(contextWithLatency.outputLatency) ? Math.max(0, contextWithLatency.outputLatency) : 0;
221
+ return Math.max(
222
+ MIN_TRANSITION_SETTLE_MS,
223
+ Math.ceil((baseLatency + outputLatency) * 1e3) + 8
224
+ );
225
+ }
226
+ var LazyWebAudioGainPort = class {
227
+ #element;
228
+ #createAudioContext;
229
+ #onContextStateChange;
230
+ #pendingTransitions = /* @__PURE__ */ new Set();
231
+ #context = null;
232
+ #source = null;
233
+ #gain = null;
234
+ #value = 1;
235
+ constructor(element, options = {}) {
236
+ this.#element = element;
237
+ this.#createAudioContext = options.createAudioContext ?? (() => {
238
+ const audioWindow = window;
239
+ const AudioContextConstructor = audioWindow.AudioContext ?? audioWindow.webkitAudioContext;
240
+ if (AudioContextConstructor === void 0) {
241
+ throw new Error("Web Audio is not supported.");
242
+ }
243
+ return new AudioContextConstructor();
244
+ });
245
+ this.#onContextStateChange = options.onContextStateChange;
246
+ this.#notifyState();
247
+ }
248
+ activateFromUserGesture() {
249
+ if (this.#context === null) {
250
+ const context = this.#createAudioContext();
251
+ const gain = context.createGain();
252
+ const source = context.createMediaElementSource(this.#element);
253
+ gain.gain.value = this.#value;
254
+ source.connect(gain);
255
+ gain.connect(context.destination);
256
+ this.#context = context;
257
+ this.#source = source;
258
+ this.#gain = gain;
259
+ context.addEventListener("statechange", this.#notifyState);
260
+ this.#notifyState();
261
+ }
262
+ if (this.#context.state === "suspended") {
263
+ return this.#context.resume().then(() => {
264
+ this.#notifyState();
265
+ });
266
+ }
267
+ return Promise.resolve();
268
+ }
269
+ async resumeIfSuspended() {
270
+ if (this.#context === null || this.#context.state !== "suspended") {
271
+ return false;
272
+ }
273
+ await this.#context.resume();
274
+ this.#notifyState();
275
+ return true;
276
+ }
277
+ getValue() {
278
+ return this.#gain?.gain.value ?? this.#value;
279
+ }
280
+ getContextState() {
281
+ return this.#context?.state ?? "uninitialized";
282
+ }
283
+ setValue(value) {
284
+ this.cancelTransition();
285
+ this.#value = clampGain(value);
286
+ if (this.#context !== null && this.#gain !== null) {
287
+ const now = this.#context.currentTime;
288
+ this.#gain.gain.setValueAtTime(this.#value, now);
289
+ }
290
+ }
291
+ cancelTransition() {
292
+ if (this.#context !== null && this.#gain !== null) {
293
+ const now = this.#context.currentTime;
294
+ const heldValue = this.#gain.gain.value;
295
+ this.#gain.gain.cancelScheduledValues(0);
296
+ this.#gain.gain.setValueAtTime(heldValue, now);
297
+ }
298
+ for (const pending of this.#pendingTransitions) {
299
+ clearTimeout(pending.timerId);
300
+ pending.resolve();
301
+ }
302
+ this.#pendingTransitions.clear();
303
+ }
304
+ transitionTo(value, transition) {
305
+ const target = clampGain(value);
306
+ const durationMs = Math.max(0, transition.durationMs);
307
+ if (this.#context === null || this.#gain === null || durationMs === 0) {
308
+ this.setValue(target);
309
+ return Promise.resolve();
310
+ }
311
+ this.cancelTransition();
312
+ const start = this.#gain.gain.value;
313
+ const durationSec = durationMs / 1e3;
314
+ this.#gain.gain.setValueCurveAtTime(
315
+ createCurve(start, target, transition.curve),
316
+ this.#context.currentTime,
317
+ durationSec
318
+ );
319
+ this.#value = target;
320
+ return new Promise((resolve) => {
321
+ const pending = {
322
+ timerId: setTimeout(() => {
323
+ this.#pendingTransitions.delete(pending);
324
+ resolve();
325
+ }, durationMs + transitionSettleMs(this.#context)),
326
+ resolve
327
+ };
328
+ this.#pendingTransitions.add(pending);
329
+ });
330
+ }
331
+ async destroy() {
332
+ this.cancelTransition();
333
+ const context = this.#context;
334
+ if (context === null) {
335
+ return;
336
+ }
337
+ context.removeEventListener("statechange", this.#notifyState);
338
+ this.#source?.disconnect();
339
+ this.#gain?.disconnect();
340
+ this.#source = null;
341
+ this.#gain = null;
342
+ this.#context = null;
343
+ await context.close();
344
+ this.#notifyState();
345
+ }
346
+ #notifyState = () => {
347
+ this.#onContextStateChange?.(
348
+ this.#context?.state ?? "uninitialized"
349
+ );
350
+ };
351
+ };
352
+
353
+ // src/adapters/browser/media-session-controller.ts
354
+ var PLAYER_MEDIA_SESSION_ACTIONS = [
355
+ "play",
356
+ "pause",
357
+ "stop",
358
+ "previoustrack",
359
+ "nexttrack",
360
+ "seekto",
361
+ "seekbackward",
362
+ "seekforward"
363
+ ];
364
+ var MUSIC_MEDIA_SESSION_ACTIONS = [
365
+ "play",
366
+ "pause",
367
+ "stop",
368
+ "previoustrack",
369
+ "nexttrack",
370
+ "seekto"
371
+ ];
372
+ function observeCommand(command, action, onError) {
373
+ void command.catch(() => {
374
+ onError?.(action);
375
+ });
376
+ }
377
+ function defaultSeekOffset(state, direction) {
378
+ if (state.mediaKind !== "podcastEpisode") {
379
+ return 10;
380
+ }
381
+ return direction === "backward" ? state.experienceConfig.podcastBackSec : state.experienceConfig.podcastForwardSec;
382
+ }
383
+ function playbackStateFor(state) {
384
+ if (state.currentItem === null) {
385
+ return "none";
386
+ }
387
+ return state.status === "playing" ? "playing" : "paused";
388
+ }
389
+ var BrowserMediaSessionController = class {
390
+ #commands;
391
+ #getState;
392
+ #session;
393
+ #createMetadata;
394
+ #registeredActions = /* @__PURE__ */ new Set();
395
+ #onAction;
396
+ #onActionError;
397
+ #metadataIdentity = null;
398
+ #actionsEnabled = false;
399
+ constructor(options) {
400
+ this.#commands = options.commands;
401
+ this.#getState = options.getState;
402
+ this.#session = options.mediaSession;
403
+ this.#createMetadata = options.createMetadata;
404
+ this.#onAction = options.onAction;
405
+ this.#onActionError = options.onActionError;
406
+ }
407
+ get supported() {
408
+ return this.#session !== void 0;
409
+ }
410
+ get actionsEnabled() {
411
+ return this.#actionsEnabled;
412
+ }
413
+ enableActionHandlers() {
414
+ if (this.#actionsEnabled || this.#session === void 0) {
415
+ return;
416
+ }
417
+ this.#actionsEnabled = true;
418
+ this.#synchronizeActionHandlers(this.#getState());
419
+ }
420
+ update(state) {
421
+ const session = this.#session;
422
+ if (session === void 0) {
423
+ return;
424
+ }
425
+ try {
426
+ session.playbackState = playbackStateFor(state);
427
+ } catch {
428
+ }
429
+ this.#updateMetadata(state);
430
+ this.#updatePosition(state);
431
+ if (this.#actionsEnabled) {
432
+ this.#synchronizeActionHandlers(state);
433
+ }
434
+ }
435
+ destroy() {
436
+ const session = this.#session;
437
+ if (session === void 0) {
438
+ return;
439
+ }
440
+ for (const action of this.#registeredActions) {
441
+ try {
442
+ session.setActionHandler(action, null);
443
+ } catch {
444
+ }
445
+ }
446
+ this.#registeredActions.clear();
447
+ this.#actionsEnabled = false;
448
+ try {
449
+ session.metadata = null;
450
+ session.playbackState = "none";
451
+ session.setPositionState();
452
+ } catch {
453
+ }
454
+ }
455
+ #createHandler(action) {
456
+ const reportAction = () => {
457
+ this.#onAction?.(action);
458
+ };
459
+ switch (action) {
460
+ case "play":
461
+ return () => {
462
+ reportAction();
463
+ observeCommand(this.#commands.play(), action, this.#onActionError);
464
+ };
465
+ case "pause":
466
+ return () => {
467
+ reportAction();
468
+ observeCommand(this.#commands.pause(), action, this.#onActionError);
469
+ };
470
+ case "stop":
471
+ return () => {
472
+ reportAction();
473
+ observeCommand(
474
+ this.#commands.stop({ resetPosition: true }),
475
+ action,
476
+ this.#onActionError
477
+ );
478
+ };
479
+ case "previoustrack":
480
+ return () => {
481
+ reportAction();
482
+ observeCommand(
483
+ this.#commands.previous(),
484
+ action,
485
+ this.#onActionError
486
+ );
487
+ };
488
+ case "nexttrack":
489
+ return () => {
490
+ reportAction();
491
+ observeCommand(this.#commands.next(), action, this.#onActionError);
492
+ };
493
+ case "seekto":
494
+ return (details) => {
495
+ reportAction();
496
+ if (details.seekTime !== void 0 && Number.isFinite(details.seekTime)) {
497
+ this.#commands.seekTo(details.seekTime);
498
+ }
499
+ };
500
+ case "seekbackward":
501
+ return (details) => {
502
+ reportAction();
503
+ const fallback = defaultSeekOffset(
504
+ this.#getState(),
505
+ "backward"
506
+ );
507
+ const offset = details.seekOffset !== void 0 && Number.isFinite(details.seekOffset) ? Math.max(details.seekOffset, 0) : fallback;
508
+ this.#commands.seekBy(-offset);
509
+ };
510
+ case "seekforward":
511
+ return (details) => {
512
+ reportAction();
513
+ const fallback = defaultSeekOffset(
514
+ this.#getState(),
515
+ "forward"
516
+ );
517
+ const offset = details.seekOffset !== void 0 && Number.isFinite(details.seekOffset) ? Math.max(details.seekOffset, 0) : fallback;
518
+ this.#commands.seekBy(offset);
519
+ };
520
+ }
521
+ }
522
+ #synchronizeActionHandlers(state) {
523
+ const session = this.#session;
524
+ if (session === void 0) {
525
+ return;
526
+ }
527
+ const desiredActions = new Set(
528
+ state.mediaKind === "podcastEpisode" ? PLAYER_MEDIA_SESSION_ACTIONS : MUSIC_MEDIA_SESSION_ACTIONS
529
+ );
530
+ for (const action of PLAYER_MEDIA_SESSION_ACTIONS) {
531
+ const shouldRegister = desiredActions.has(action);
532
+ const isRegistered = this.#registeredActions.has(action);
533
+ if (shouldRegister === isRegistered) {
534
+ continue;
535
+ }
536
+ try {
537
+ session.setActionHandler(
538
+ action,
539
+ shouldRegister ? this.#createHandler(action) : null
540
+ );
541
+ if (shouldRegister) {
542
+ this.#registeredActions.add(action);
543
+ } else {
544
+ this.#registeredActions.delete(action);
545
+ }
546
+ } catch {
547
+ }
548
+ }
549
+ }
550
+ #updateMetadata(state) {
551
+ const session = this.#session;
552
+ const item = state.currentItem;
553
+ if (session === void 0) {
554
+ return;
555
+ }
556
+ if (item === null) {
557
+ if (this.#metadataIdentity !== null) {
558
+ try {
559
+ session.metadata = null;
560
+ } catch {
561
+ }
562
+ this.#metadataIdentity = null;
563
+ }
564
+ return;
565
+ }
566
+ const identity = [
567
+ item.id,
568
+ item.title,
569
+ item.creatorName ?? "",
570
+ item.collectionTitle ?? "",
571
+ item.artwork?.url ?? ""
572
+ ].join("\0");
573
+ if (identity === this.#metadataIdentity || this.#createMetadata === void 0) {
574
+ return;
575
+ }
576
+ try {
577
+ session.metadata = this.#createMetadata({
578
+ title: item.title,
579
+ artist: item.creatorName ?? "",
580
+ album: item.collectionTitle ?? "",
581
+ artwork: item.artwork === void 0 ? [] : [{ src: item.artwork.url }]
582
+ });
583
+ this.#metadataIdentity = identity;
584
+ } catch {
585
+ }
586
+ }
587
+ #updatePosition(state) {
588
+ const session = this.#session;
589
+ if (session === void 0) {
590
+ return;
591
+ }
592
+ const duration = state.durationSec;
593
+ if (duration === null || !Number.isFinite(duration) || duration <= 0) {
594
+ try {
595
+ session.setPositionState();
596
+ } catch {
597
+ }
598
+ return;
599
+ }
600
+ const position = Math.min(
601
+ Math.max(
602
+ Number.isFinite(state.currentTimeSec) ? state.currentTimeSec : 0,
603
+ 0
604
+ ),
605
+ duration
606
+ );
607
+ try {
608
+ session.setPositionState({
609
+ duration,
610
+ playbackRate: 1,
611
+ position
612
+ });
613
+ } catch {
614
+ }
615
+ }
616
+ };
617
+ function createBrowserMediaSessionController(options) {
618
+ const mediaSession = typeof navigator !== "undefined" && "mediaSession" in navigator ? navigator.mediaSession : void 0;
619
+ const createMetadata = typeof MediaMetadata === "function" ? (init) => new MediaMetadata(init) : void 0;
620
+ return new BrowserMediaSessionController({
621
+ ...options,
622
+ ...mediaSession === void 0 ? {} : { mediaSession },
623
+ ...createMetadata === void 0 ? {} : { createMetadata }
624
+ });
625
+ }
626
+
627
+ // src/config/defaults.ts
628
+ var PLAYER_EXPERIENCE_CONFIG_LIMITS = Object.freeze({
629
+ fadeMs: Object.freeze({ min: 0, max: 1e4 }),
630
+ previousRestartThresholdSec: Object.freeze({ min: 0, max: 60 }),
631
+ waveformOpacity: Object.freeze({ min: 0, max: 1 }),
632
+ initialVolume: Object.freeze({ min: 0, max: 1 }),
633
+ podcastSkipSec: Object.freeze({ minExclusive: 0, max: 3600 })
634
+ });
635
+ var PREVIEW_PLAYER_EXPERIENCE_CONFIG = Object.freeze({
636
+ playFadeInMs: 300,
637
+ pauseFadeOutMs: 600,
638
+ trackChangeFadeOutMs: 300,
639
+ trackChangeFadeInMs: 300,
640
+ seekFadeMs: 120,
641
+ fadeCurve: "ease-out",
642
+ previousRestartThresholdSec: 3,
643
+ seekCommitMode: "on-release",
644
+ waveformPlayedStateEnabled: true,
645
+ waveformOpacity: 0.72,
646
+ initialShuffleEnabled: false,
647
+ initialRepeatMode: "off",
648
+ initialVolume: 0.8,
649
+ persistMode: "none",
650
+ podcastBackSec: 15,
651
+ podcastForwardSec: 30,
652
+ podcastPreviousMode: "previous-episode"
653
+ });
654
+
655
+ // src/config/validation.ts
656
+ var CONFIG_KEYS = [
657
+ "playFadeInMs",
658
+ "pauseFadeOutMs",
659
+ "trackChangeFadeOutMs",
660
+ "trackChangeFadeInMs",
661
+ "seekFadeMs",
662
+ "fadeCurve",
663
+ "previousRestartThresholdSec",
664
+ "seekCommitMode",
665
+ "waveformPlayedStateEnabled",
666
+ "waveformOpacity",
667
+ "initialShuffleEnabled",
668
+ "initialRepeatMode",
669
+ "initialVolume",
670
+ "persistMode",
671
+ "podcastBackSec",
672
+ "podcastForwardSec",
673
+ "podcastPreviousMode"
674
+ ];
675
+ var FADE_CURVES = /* @__PURE__ */ new Set([
676
+ "linear",
677
+ "ease-in",
678
+ "ease-out",
679
+ "equal-power"
680
+ ]);
681
+ var SEEK_COMMIT_MODES = /* @__PURE__ */ new Set(["live", "on-release"]);
682
+ var REPEAT_MODES = /* @__PURE__ */ new Set(["off", "all", "one"]);
683
+ var PERSIST_MODES = /* @__PURE__ */ new Set(["none", "session", "local"]);
684
+ var PODCAST_PREVIOUS_MODES = /* @__PURE__ */ new Set([
685
+ "previous-episode",
686
+ "restart-episode"
687
+ ]);
688
+ function isRecord(value) {
689
+ return typeof value === "object" && value !== null && !Array.isArray(value);
690
+ }
691
+ function validateFiniteRange(value, path, min, max, errors) {
692
+ if (typeof value !== "number" || !Number.isFinite(value) || value < min || value > max) {
693
+ errors.push(
694
+ `${path} must be a finite number between ${String(min)} and ${String(max)}.`
695
+ );
696
+ return false;
697
+ }
698
+ return true;
699
+ }
700
+ function validateBoolean(value, path, errors) {
701
+ if (typeof value !== "boolean") {
702
+ errors.push(`${path} must be a boolean.`);
703
+ return false;
704
+ }
705
+ return true;
706
+ }
707
+ function validateConfigRecord(value, options) {
708
+ if (!isRecord(value)) {
709
+ return { ok: false, errors: ["config must be an object."] };
710
+ }
711
+ const errors = [];
712
+ const allowedKeys = new Set(CONFIG_KEYS);
713
+ for (const key of Object.keys(value)) {
714
+ if (!allowedKeys.has(key)) {
715
+ errors.push(`config.${key} is not supported.`);
716
+ }
717
+ }
718
+ if (!options.partial) {
719
+ for (const key of CONFIG_KEYS) {
720
+ if (!Object.hasOwn(value, key)) {
721
+ errors.push(`config.${key} is required.`);
722
+ }
723
+ }
724
+ }
725
+ const has = (key) => Object.hasOwn(value, key);
726
+ const fadeLimits = PLAYER_EXPERIENCE_CONFIG_LIMITS.fadeMs;
727
+ for (const key of [
728
+ "playFadeInMs",
729
+ "pauseFadeOutMs",
730
+ "trackChangeFadeOutMs",
731
+ "trackChangeFadeInMs",
732
+ "seekFadeMs"
733
+ ]) {
734
+ if (has(key)) {
735
+ validateFiniteRange(value[key], `config.${key}`, fadeLimits.min, fadeLimits.max, errors);
736
+ }
737
+ }
738
+ const thresholdLimits = PLAYER_EXPERIENCE_CONFIG_LIMITS.previousRestartThresholdSec;
739
+ if (has("previousRestartThresholdSec")) {
740
+ validateFiniteRange(
741
+ value.previousRestartThresholdSec,
742
+ "config.previousRestartThresholdSec",
743
+ thresholdLimits.min,
744
+ thresholdLimits.max,
745
+ errors
746
+ );
747
+ }
748
+ const volumeLimits = PLAYER_EXPERIENCE_CONFIG_LIMITS.initialVolume;
749
+ if (has("initialVolume")) {
750
+ validateFiniteRange(
751
+ value.initialVolume,
752
+ "config.initialVolume",
753
+ volumeLimits.min,
754
+ volumeLimits.max,
755
+ errors
756
+ );
757
+ }
758
+ const waveformOpacityLimits = PLAYER_EXPERIENCE_CONFIG_LIMITS.waveformOpacity;
759
+ if (has("waveformOpacity")) {
760
+ validateFiniteRange(
761
+ value.waveformOpacity,
762
+ "config.waveformOpacity",
763
+ waveformOpacityLimits.min,
764
+ waveformOpacityLimits.max,
765
+ errors
766
+ );
767
+ }
768
+ for (const key of [
769
+ "waveformPlayedStateEnabled",
770
+ "initialShuffleEnabled"
771
+ ]) {
772
+ if (has(key)) {
773
+ validateBoolean(value[key], `config.${key}`, errors);
774
+ }
775
+ }
776
+ const skipLimits = PLAYER_EXPERIENCE_CONFIG_LIMITS.podcastSkipSec;
777
+ for (const key of ["podcastBackSec", "podcastForwardSec"]) {
778
+ if (has(key)) {
779
+ const candidate = value[key];
780
+ const valid = validateFiniteRange(
781
+ candidate,
782
+ `config.${key}`,
783
+ skipLimits.minExclusive,
784
+ skipLimits.max,
785
+ errors
786
+ );
787
+ if (valid && candidate <= skipLimits.minExclusive) {
788
+ errors.push(`config.${key} must be greater than 0.`);
789
+ }
790
+ }
791
+ }
792
+ if (has("fadeCurve") && !FADE_CURVES.has(value.fadeCurve)) {
793
+ errors.push("config.fadeCurve is not supported.");
794
+ }
795
+ if (has("seekCommitMode") && !SEEK_COMMIT_MODES.has(value.seekCommitMode)) {
796
+ errors.push("config.seekCommitMode is not supported.");
797
+ }
798
+ if (has("initialRepeatMode") && !REPEAT_MODES.has(value.initialRepeatMode)) {
799
+ errors.push("config.initialRepeatMode is not supported.");
800
+ }
801
+ if (has("persistMode") && !PERSIST_MODES.has(value.persistMode)) {
802
+ errors.push("config.persistMode is not supported.");
803
+ }
804
+ if (has("podcastPreviousMode") && !PODCAST_PREVIOUS_MODES.has(
805
+ value.podcastPreviousMode
806
+ )) {
807
+ errors.push("config.podcastPreviousMode is not supported.");
808
+ }
809
+ if (errors.length > 0) {
810
+ return { ok: false, errors: Object.freeze(errors) };
811
+ }
812
+ const merged = options.partial ? { ...PREVIEW_PLAYER_EXPERIENCE_CONFIG, ...value } : value;
813
+ return {
814
+ ok: true,
815
+ errors: [],
816
+ value: Object.freeze({ ...merged })
817
+ };
818
+ }
819
+ function validatePlayerExperienceConfig(value) {
820
+ return validateConfigRecord(value, { partial: false });
821
+ }
822
+ function validatePlayerExperienceConfigOverride(value) {
823
+ return validateConfigRecord(value, { partial: true });
824
+ }
825
+ function resolvePlayerExperienceConfig(override) {
826
+ return validatePlayerExperienceConfigOverride(override ?? {});
827
+ }
828
+
829
+ // src/config/initial-state.ts
830
+ function createInitialPlayerState(experienceConfig) {
831
+ const resolved = resolvePlayerExperienceConfig(experienceConfig);
832
+ if (!resolved.ok) {
833
+ throw new TypeError(resolved.errors.join(" "));
834
+ }
835
+ return Object.freeze({
836
+ currentItem: null,
837
+ currentItemId: null,
838
+ currentQueueItemId: null,
839
+ mediaKind: null,
840
+ status: "idle",
841
+ currentTimeSec: 0,
842
+ durationSec: null,
843
+ volume: resolved.value.initialVolume,
844
+ muted: false,
845
+ shuffleEnabled: resolved.value.initialShuffleEnabled,
846
+ repeatMode: resolved.value.initialRepeatMode,
847
+ queue: Object.freeze([]),
848
+ currentQueueIndex: null,
849
+ canSeek: false,
850
+ canGoPrevious: false,
851
+ canGoNext: false,
852
+ needsUserGesture: false,
853
+ lastTransitionCause: null,
854
+ error: null,
855
+ experienceConfig: resolved.value
856
+ });
857
+ }
858
+
859
+ // src/core/fade.ts
860
+ var AUDIO_CONTEXT_ERROR = Object.freeze({
861
+ code: "audio-context",
862
+ recoverable: true,
863
+ messageKey: "player.error.audio-context"
864
+ });
865
+ async function runFade(gain, request) {
866
+ if (gain === void 0) {
867
+ return { completed: true, error: null };
868
+ }
869
+ try {
870
+ if (request.durationMs <= 0) {
871
+ gain.setValue(request.target);
872
+ } else {
873
+ await gain.transitionTo(request.target, {
874
+ durationMs: request.durationMs,
875
+ curve: request.curve
876
+ });
877
+ }
878
+ return { completed: true, error: null };
879
+ } catch {
880
+ return { completed: false, error: AUDIO_CONTEXT_ERROR };
881
+ }
882
+ }
883
+
884
+ // src/core/media-events.ts
885
+ var PLAYER_MEDIA_EVENT_TYPES = Object.freeze([
886
+ "loadstart",
887
+ "loadedmetadata",
888
+ "canplay",
889
+ "play",
890
+ "playing",
891
+ "pause",
892
+ "waiting",
893
+ "stalled",
894
+ "seeking",
895
+ "seeked",
896
+ "timeupdate",
897
+ "ended",
898
+ "error",
899
+ "durationchange",
900
+ "volumechange"
901
+ ]);
902
+ var PLAY_ERROR_DETAILS = Object.freeze({
903
+ NotAllowedError: Object.freeze({
904
+ code: "autoplay-blocked",
905
+ recoverable: true
906
+ }),
907
+ NotSupportedError: Object.freeze({
908
+ code: "unsupported",
909
+ recoverable: false
910
+ }),
911
+ AbortError: Object.freeze({
912
+ code: "unknown",
913
+ recoverable: true
914
+ })
915
+ });
916
+ function createPlayerError(code, recoverable) {
917
+ return Object.freeze({
918
+ code,
919
+ recoverable,
920
+ messageKey: `player.error.${code}`
921
+ });
922
+ }
923
+ function classifyPlayError(error, media) {
924
+ const mediaError = media.getError();
925
+ if (mediaError !== null) {
926
+ return mediaError;
927
+ }
928
+ const name = typeof error === "object" && error !== null && "name" in error && typeof error.name === "string" ? error.name : "";
929
+ const detail = PLAY_ERROR_DETAILS[name];
930
+ return detail === void 0 ? createPlayerError("unknown", true) : createPlayerError(detail.code, detail.recoverable);
931
+ }
932
+ function readFiniteCurrentTime(media) {
933
+ const value = media.getCurrentTimeSec();
934
+ return Number.isFinite(value) && value >= 0 ? value : 0;
935
+ }
936
+ function readFiniteDuration(media) {
937
+ const value = media.getDurationSec();
938
+ return value !== null && Number.isFinite(value) && value >= 0 ? value : null;
939
+ }
940
+
941
+ // src/core/persistence.ts
942
+ var DEFAULT_PODCAST_PROGRESS_SAVE_INTERVAL_SEC = 5;
943
+ function isUsablePodcastProgress(progress, item) {
944
+ return progress !== null && progress.itemId === item.id && Number.isFinite(progress.positionSec) && progress.positionSec >= 0;
945
+ }
946
+ function createPodcastProgress(item, positionSec, now) {
947
+ return Object.freeze({
948
+ itemId: item.id,
949
+ positionSec,
950
+ updatedAt: now().toISOString()
951
+ });
952
+ }
953
+ function canUsePodcastProgress(item, port, persistMode) {
954
+ return item !== null && item.kind === "podcastEpisode" && port !== void 0 && persistMode !== "none";
955
+ }
956
+
957
+ // src/core/queue.ts
958
+ function createLoadedQueueItem(item) {
959
+ return Object.freeze({
960
+ queueItemId: `loaded:${item.id}`,
961
+ item
962
+ });
963
+ }
964
+ function freezeQueue(queue) {
965
+ return Object.freeze([...queue]);
966
+ }
967
+ function findQueueIndex(queue, queueItemId) {
968
+ const index = queue.findIndex((entry) => entry.queueItemId === queueItemId);
969
+ return index < 0 ? null : index;
970
+ }
971
+ function hasUniqueQueueItemIds(queue) {
972
+ return new Set(queue.map((entry) => entry.queueItemId)).size === queue.length;
973
+ }
974
+ function getNextQueueIndex(queue, currentIndex, repeatMode) {
975
+ if (queue.length === 0 || currentIndex < 0) {
976
+ return null;
977
+ }
978
+ if (currentIndex < queue.length - 1) {
979
+ return currentIndex + 1;
980
+ }
981
+ return repeatMode === "all" ? 0 : null;
982
+ }
983
+ function getPreviousQueueIndex(queue, currentIndex) {
984
+ if (queue.length === 0 || currentIndex <= 0) {
985
+ return null;
986
+ }
987
+ return currentIndex - 1;
988
+ }
989
+
990
+ // src/core/player-reducer.ts
991
+ function deriveCurrentIndex(queue, queueItemId) {
992
+ if (queueItemId === null) {
993
+ return null;
994
+ }
995
+ const index = queue.findIndex((entry) => entry.queueItemId === queueItemId);
996
+ return index < 0 ? null : index;
997
+ }
998
+ function createPlayerSnapshot(previous, patch) {
999
+ const queue = patch.queue === void 0 ? previous.queue : freezeQueue(patch.queue);
1000
+ const currentQueueItemId = patch.currentQueueItemId === void 0 ? previous.currentQueueItemId : patch.currentQueueItemId;
1001
+ const currentQueueIndex = deriveCurrentIndex(queue, currentQueueItemId);
1002
+ const repeatMode = patch.repeatMode ?? previous.repeatMode;
1003
+ const currentItem = patch.currentItem === void 0 ? previous.currentItem : patch.currentItem;
1004
+ return Object.freeze({
1005
+ ...previous,
1006
+ ...patch,
1007
+ queue,
1008
+ currentQueueItemId,
1009
+ currentQueueIndex,
1010
+ canGoPrevious: patch.canGoPrevious ?? currentItem !== null,
1011
+ canGoNext: patch.canGoNext ?? (currentQueueIndex !== null && (currentQueueIndex < queue.length - 1 || queue.length > 0 && repeatMode === "all"))
1012
+ });
1013
+ }
1014
+
1015
+ // src/core/shuffle.ts
1016
+ function normalizeRandom(value) {
1017
+ if (!Number.isFinite(value)) {
1018
+ return 0;
1019
+ }
1020
+ return Math.min(Math.max(value, 0), 0.999999999999);
1021
+ }
1022
+ function shuffleQueueAroundCurrent(queue, currentQueueItemId, random) {
1023
+ const current = queue.find(
1024
+ (entry) => entry.queueItemId === currentQueueItemId
1025
+ );
1026
+ if (current === void 0) {
1027
+ return Object.freeze([...queue]);
1028
+ }
1029
+ const remaining = queue.filter(
1030
+ (entry) => entry.queueItemId !== currentQueueItemId
1031
+ );
1032
+ for (let index = remaining.length - 1; index > 0; index -= 1) {
1033
+ const swapIndex = Math.floor(normalizeRandom(random()) * (index + 1));
1034
+ const candidate = remaining[index];
1035
+ const replacement = remaining[swapIndex];
1036
+ if (candidate !== void 0 && replacement !== void 0) {
1037
+ remaining[index] = replacement;
1038
+ remaining[swapIndex] = candidate;
1039
+ }
1040
+ }
1041
+ return Object.freeze([current, ...remaining]);
1042
+ }
1043
+
1044
+ // src/core/create-player-store.ts
1045
+ var OK_RESULT = Object.freeze({ ok: true });
1046
+ var NO_ITEM_ERROR = createPlayerError("unknown", false);
1047
+ var INVALID_QUEUE_ERROR = Object.freeze({
1048
+ code: "unknown",
1049
+ recoverable: false,
1050
+ messageKey: "player.error.invalid-queue"
1051
+ });
1052
+ var DESTROYED_ERROR = Object.freeze({
1053
+ code: "unknown",
1054
+ recoverable: false,
1055
+ messageKey: "player.error.destroyed"
1056
+ });
1057
+ var MAX_SHUFFLE_HISTORY_ITEMS = 512;
1058
+ var RETAINED_SHUFFLE_HISTORY_ITEMS = 128;
1059
+ function failed(error) {
1060
+ return Object.freeze({ ok: false, error });
1061
+ }
1062
+ function clampVolume2(value) {
1063
+ if (!Number.isFinite(value)) {
1064
+ return 0;
1065
+ }
1066
+ return Math.min(Math.max(value, 0), 1);
1067
+ }
1068
+ var CorePlayerStore = class {
1069
+ commands;
1070
+ experience;
1071
+ #media;
1072
+ #gain;
1073
+ #podcastProgress;
1074
+ #random;
1075
+ #now;
1076
+ #podcastProgressSaveIntervalSec;
1077
+ #listeners = /* @__PURE__ */ new Set();
1078
+ #experienceListeners = /* @__PURE__ */ new Set();
1079
+ #mediaListener = (event) => {
1080
+ this.#handleMediaEvent(event);
1081
+ };
1082
+ #state;
1083
+ #playbackOrderQueueItemIds = Object.freeze([]);
1084
+ #playbackOrderCursor = -1;
1085
+ #commandVersion = 0;
1086
+ #desiredPlayback = "paused";
1087
+ #ignoreNextPauseEvent = false;
1088
+ #handlingEnded = false;
1089
+ #destroyed = false;
1090
+ #lastSavedPodcastPositions = /* @__PURE__ */ new Map();
1091
+ #progressSaveChain = Promise.resolve();
1092
+ constructor(options) {
1093
+ this.#media = options.media;
1094
+ this.#gain = options.gain;
1095
+ this.#podcastProgress = options.podcastProgress;
1096
+ this.#random = options.random ?? Math.random;
1097
+ this.#now = options.now ?? (() => /* @__PURE__ */ new Date());
1098
+ this.#podcastProgressSaveIntervalSec = options.podcastProgressSaveIntervalSec ?? DEFAULT_PODCAST_PROGRESS_SAVE_INTERVAL_SEC;
1099
+ if (!Number.isFinite(this.#podcastProgressSaveIntervalSec) || this.#podcastProgressSaveIntervalSec <= 0) {
1100
+ throw new RangeError(
1101
+ "podcastProgressSaveIntervalSec must be a positive finite number."
1102
+ );
1103
+ }
1104
+ this.#state = createInitialPlayerState(options.experienceConfig);
1105
+ this.#media.setVolume(this.#state.volume);
1106
+ this.#media.setMuted(this.#state.muted);
1107
+ try {
1108
+ this.#gain?.setValue(1);
1109
+ } catch {
1110
+ this.#state = createPlayerSnapshot(this.#state, {
1111
+ error: createPlayerError("audio-context", true)
1112
+ });
1113
+ }
1114
+ for (const eventType of PLAYER_MEDIA_EVENT_TYPES) {
1115
+ this.#media.addEventListener(eventType, this.#mediaListener);
1116
+ }
1117
+ const commands = {
1118
+ load: (item) => this.#load(item),
1119
+ setQueue: (queue, commandOptions) => this.#setQueue(queue, commandOptions),
1120
+ play: () => this.#play(),
1121
+ pause: (commandOptions) => this.#pause(commandOptions),
1122
+ stop: (commandOptions) => this.#stop(commandOptions),
1123
+ togglePlayback: () => this.#togglePlayback(),
1124
+ seekTo: (seconds) => {
1125
+ this.#seekTo(seconds);
1126
+ },
1127
+ seekBy: (deltaSeconds) => {
1128
+ this.#seekBy(deltaSeconds);
1129
+ },
1130
+ setVolume: (volume) => {
1131
+ this.#setVolume(volume);
1132
+ },
1133
+ toggleMute: () => {
1134
+ this.#toggleMute();
1135
+ },
1136
+ setShuffle: (enabled) => {
1137
+ this.#setShuffle(enabled);
1138
+ },
1139
+ setRepeatMode: (mode) => {
1140
+ this.#setRepeatMode(mode);
1141
+ },
1142
+ next: () => this.#next(),
1143
+ previous: () => this.#previous(),
1144
+ retry: () => this.#retry(),
1145
+ clear: () => this.#clear()
1146
+ };
1147
+ this.commands = Object.freeze(commands);
1148
+ const experience = {
1149
+ getConfig: () => this.#state.experienceConfig,
1150
+ updateConfig: (patch) => {
1151
+ this.#updateExperienceConfig(patch);
1152
+ },
1153
+ resetConfig: () => {
1154
+ this.#replaceExperienceConfig(PREVIEW_PLAYER_EXPERIENCE_CONFIG);
1155
+ },
1156
+ subscribe: (listener) => this.#subscribeTo(this.#experienceListeners, listener)
1157
+ };
1158
+ this.experience = Object.freeze(experience);
1159
+ }
1160
+ getState() {
1161
+ return this.#state;
1162
+ }
1163
+ subscribe(listener) {
1164
+ return this.#subscribeTo(this.#listeners, listener);
1165
+ }
1166
+ async synchronizeFromMedia(reason) {
1167
+ if (this.#destroyed || this.#state.currentItem === null) {
1168
+ return;
1169
+ }
1170
+ const snapshot = this.#media.getStateSnapshot();
1171
+ if (!snapshot.hasCurrentSource || !snapshot.currentSourceMatchesAssigned) {
1172
+ const error = createPlayerError("unknown", true);
1173
+ this.#desiredPlayback = "paused";
1174
+ this.#setState({
1175
+ status: "error",
1176
+ lastTransitionCause: "source-error",
1177
+ error
1178
+ });
1179
+ return;
1180
+ }
1181
+ if (snapshot.ended) {
1182
+ await this.#handleEnded();
1183
+ return;
1184
+ }
1185
+ const wasPlaying = this.#state.status === "playing";
1186
+ const externallyPaused = wasPlaying && snapshot.paused;
1187
+ this.#desiredPlayback = snapshot.paused ? "paused" : "playing";
1188
+ this.#setState({
1189
+ status: snapshot.paused ? "paused" : "playing",
1190
+ currentTimeSec: snapshot.currentTimeSec,
1191
+ durationSec: snapshot.durationSec,
1192
+ volume: clampVolume2(snapshot.volume),
1193
+ muted: snapshot.muted,
1194
+ canSeek: snapshot.durationSec !== null,
1195
+ ...externallyPaused ? {
1196
+ lastTransitionCause: reason === "visibilitychange" || reason === "pageshow" || reason === "focus" || reason === "audio-context-statechange" ? "background-suspension" : "os-interruption"
1197
+ } : {}
1198
+ });
1199
+ }
1200
+ async destroy() {
1201
+ if (this.#destroyed) {
1202
+ return;
1203
+ }
1204
+ await this.#clear();
1205
+ this.#destroyed = true;
1206
+ this.#commandVersion += 1;
1207
+ try {
1208
+ this.#gain?.cancelTransition();
1209
+ } catch {
1210
+ }
1211
+ for (const eventType of PLAYER_MEDIA_EVENT_TYPES) {
1212
+ this.#media.removeEventListener(eventType, this.#mediaListener);
1213
+ }
1214
+ await this.#progressSaveChain;
1215
+ await this.#gain?.destroy();
1216
+ this.#listeners.clear();
1217
+ this.#experienceListeners.clear();
1218
+ }
1219
+ #subscribeTo(collection, listener) {
1220
+ if (this.#destroyed) {
1221
+ return () => void 0;
1222
+ }
1223
+ collection.add(listener);
1224
+ return () => {
1225
+ collection.delete(listener);
1226
+ };
1227
+ }
1228
+ #setState(patch) {
1229
+ const queue = patch.queue ?? this.#state.queue;
1230
+ const currentItem = patch.currentItem === void 0 ? this.#state.currentItem : patch.currentItem;
1231
+ const currentQueueItemId = patch.currentQueueItemId === void 0 ? this.#state.currentQueueItemId : patch.currentQueueItemId;
1232
+ const repeatMode = patch.repeatMode ?? this.#state.repeatMode;
1233
+ const playbackIndex = currentQueueItemId === null ? -1 : this.#playbackOrderCursor;
1234
+ this.#state = createPlayerSnapshot(this.#state, {
1235
+ ...patch,
1236
+ canGoPrevious: currentItem !== null,
1237
+ canGoNext: playbackIndex >= 0 && (playbackIndex < this.#playbackOrderQueueItemIds.length - 1 || this.#playbackOrderQueueItemIds.length > 0 && repeatMode === "all"),
1238
+ queue
1239
+ });
1240
+ for (const listener of this.#listeners) {
1241
+ listener();
1242
+ }
1243
+ }
1244
+ #beginCommand(desiredPlayback = this.#desiredPlayback) {
1245
+ this.#commandVersion += 1;
1246
+ this.#desiredPlayback = desiredPlayback;
1247
+ try {
1248
+ this.#gain?.cancelTransition();
1249
+ } catch {
1250
+ this.#setState({ error: createPlayerError("audio-context", true) });
1251
+ }
1252
+ return this.#commandVersion;
1253
+ }
1254
+ #isCurrentCommand(version) {
1255
+ return !this.#destroyed && version === this.#commandVersion;
1256
+ }
1257
+ #pauseMediaElement() {
1258
+ if (!this.#media.isPaused()) {
1259
+ this.#ignoreNextPauseEvent = true;
1260
+ }
1261
+ this.#media.pause();
1262
+ }
1263
+ #replaceExperienceConfig(config) {
1264
+ const result = validatePlayerExperienceConfig(config);
1265
+ if (!result.ok) {
1266
+ throw new TypeError(result.errors.join(" "));
1267
+ }
1268
+ this.#setState({ experienceConfig: result.value });
1269
+ for (const listener of this.#experienceListeners) {
1270
+ listener();
1271
+ }
1272
+ }
1273
+ #updateExperienceConfig(patch) {
1274
+ this.#replaceExperienceConfig({
1275
+ ...this.#state.experienceConfig,
1276
+ ...patch
1277
+ });
1278
+ }
1279
+ async #load(item) {
1280
+ return this.#setQueue([createLoadedQueueItem(item)]);
1281
+ }
1282
+ async #setQueue(queue, options = {}) {
1283
+ if (this.#destroyed) {
1284
+ return failed(DESTROYED_ERROR);
1285
+ }
1286
+ if (!hasUniqueQueueItemIds(queue)) {
1287
+ return failed(INVALID_QUEUE_ERROR);
1288
+ }
1289
+ if (queue.length === 0) {
1290
+ await this.#clear();
1291
+ return OK_RESULT;
1292
+ }
1293
+ const normalizedQueue = freezeQueue(queue);
1294
+ const startQueueItemId = options.startQueueItemId ?? normalizedQueue[0]?.queueItemId;
1295
+ const startIndex = startQueueItemId === void 0 ? null : findQueueIndex(normalizedQueue, startQueueItemId);
1296
+ if (startIndex === null) {
1297
+ return failed(INVALID_QUEUE_ERROR);
1298
+ }
1299
+ const version = this.#beginCommand(
1300
+ options.autoplay === true ? "playing" : "paused"
1301
+ );
1302
+ const playbackQueue = this.#state.shuffleEnabled && startQueueItemId !== void 0 ? shuffleQueueAroundCurrent(
1303
+ normalizedQueue,
1304
+ startQueueItemId,
1305
+ this.#random
1306
+ ) : normalizedQueue;
1307
+ this.#playbackOrderQueueItemIds = Object.freeze(
1308
+ playbackQueue.map((entry) => entry.queueItemId)
1309
+ );
1310
+ this.#playbackOrderCursor = this.#state.shuffleEnabled ? 0 : startIndex;
1311
+ this.#setState({
1312
+ queue: normalizedQueue,
1313
+ repeatMode: this.#state.repeatMode,
1314
+ error: null
1315
+ });
1316
+ return this.#selectQueueIndex(
1317
+ version,
1318
+ startIndex,
1319
+ options.autoplay === true
1320
+ );
1321
+ }
1322
+ async #selectQueueIndex(version, index, autoplay) {
1323
+ const selected = this.#state.queue[index];
1324
+ if (selected === void 0) {
1325
+ return failed(INVALID_QUEUE_ERROR);
1326
+ }
1327
+ const config = this.#state.experienceConfig;
1328
+ const replacingCurrent = this.#state.currentItem !== null;
1329
+ let transitionError = null;
1330
+ if (replacingCurrent) {
1331
+ void this.#savePodcastProgress(true);
1332
+ if (this.#state.status === "playing") {
1333
+ const fadeResult = await runFade(this.#gain, {
1334
+ target: 0,
1335
+ durationMs: config.trackChangeFadeOutMs,
1336
+ curve: config.fadeCurve
1337
+ });
1338
+ if (!this.#isCurrentCommand(version)) {
1339
+ return OK_RESULT;
1340
+ }
1341
+ if (fadeResult.error !== null) {
1342
+ transitionError = fadeResult.error;
1343
+ }
1344
+ }
1345
+ if (!this.#isCurrentCommand(version)) {
1346
+ return OK_RESULT;
1347
+ }
1348
+ this.#pauseMediaElement();
1349
+ }
1350
+ this.#setState({
1351
+ currentItem: selected.item,
1352
+ currentItemId: selected.item.id,
1353
+ currentQueueItemId: selected.queueItemId,
1354
+ mediaKind: selected.item.kind,
1355
+ status: "loading",
1356
+ currentTimeSec: 0,
1357
+ durationSec: null,
1358
+ canSeek: false,
1359
+ needsUserGesture: false,
1360
+ error: transitionError
1361
+ });
1362
+ try {
1363
+ this.#media.setSource(selected.item.source);
1364
+ this.#media.load();
1365
+ } catch {
1366
+ const error = createPlayerError("unknown", true);
1367
+ this.#setState({ status: "error", error });
1368
+ return failed(error);
1369
+ }
1370
+ await this.#restorePodcastProgress(version, selected.item);
1371
+ if (!this.#isCurrentCommand(version)) {
1372
+ return OK_RESULT;
1373
+ }
1374
+ return autoplay ? this.#playWithCommand(
1375
+ version,
1376
+ config.trackChangeFadeInMs,
1377
+ config.fadeCurve
1378
+ ) : OK_RESULT;
1379
+ }
1380
+ async #restorePodcastProgress(version, item) {
1381
+ const port = this.#podcastProgress;
1382
+ if (port === void 0 || !canUsePodcastProgress(
1383
+ item,
1384
+ port,
1385
+ this.#state.experienceConfig.persistMode
1386
+ )) {
1387
+ return;
1388
+ }
1389
+ try {
1390
+ const progress = await port.load(item.id);
1391
+ if (!this.#isCurrentCommand(version) || this.#state.currentItemId !== item.id || !isUsablePodcastProgress(progress, item)) {
1392
+ return;
1393
+ }
1394
+ const duration = readFiniteDuration(this.#media);
1395
+ const target = duration === null ? progress.positionSec : Math.min(progress.positionSec, duration);
1396
+ this.#media.seekTo(target);
1397
+ this.#lastSavedPodcastPositions.set(item.id, target);
1398
+ this.#setState({ currentTimeSec: target });
1399
+ } catch {
1400
+ }
1401
+ }
1402
+ async #play() {
1403
+ if (this.#destroyed) {
1404
+ return failed(DESTROYED_ERROR);
1405
+ }
1406
+ const version = this.#beginCommand("playing");
1407
+ const config = this.#state.experienceConfig;
1408
+ const result = await this.#playWithCommand(
1409
+ version,
1410
+ config.playFadeInMs,
1411
+ config.fadeCurve
1412
+ );
1413
+ if (result.ok && this.#isCurrentCommand(version)) {
1414
+ this.#setState({ lastTransitionCause: "user-play" });
1415
+ }
1416
+ return result;
1417
+ }
1418
+ async #playWithCommand(version, fadeDurationMs, fadeCurve) {
1419
+ if (this.#state.currentItem === null) {
1420
+ this.#desiredPlayback = "paused";
1421
+ return failed(NO_ITEM_ERROR);
1422
+ }
1423
+ if (this.#gain !== void 0) {
1424
+ try {
1425
+ this.#gain.setValue(fadeDurationMs > 0 ? 0 : 1);
1426
+ } catch {
1427
+ this.#setState({ error: createPlayerError("audio-context", true) });
1428
+ }
1429
+ }
1430
+ try {
1431
+ await this.#media.play();
1432
+ } catch (error) {
1433
+ if (!this.#isCurrentCommand(version)) {
1434
+ return OK_RESULT;
1435
+ }
1436
+ const playerError = classifyPlayError(error, this.#media);
1437
+ this.#desiredPlayback = "paused";
1438
+ this.#restoreGain();
1439
+ this.#setState({
1440
+ status: "error",
1441
+ needsUserGesture: playerError.code === "autoplay-blocked",
1442
+ ...playerError.code === "autoplay-blocked" ? { lastTransitionCause: "autoplay-blocked" } : {},
1443
+ error: playerError
1444
+ });
1445
+ return failed(playerError);
1446
+ }
1447
+ if (!this.#isCurrentCommand(version)) {
1448
+ if (this.#desiredPlayback === "paused") {
1449
+ this.#pauseMediaElement();
1450
+ }
1451
+ return OK_RESULT;
1452
+ }
1453
+ this.#setState({
1454
+ status: "playing",
1455
+ needsUserGesture: false,
1456
+ error: null
1457
+ });
1458
+ const fadeResult = await runFade(this.#gain, {
1459
+ target: 1,
1460
+ durationMs: fadeDurationMs,
1461
+ curve: fadeCurve
1462
+ });
1463
+ if (this.#isCurrentCommand(version) && fadeResult.error !== null) {
1464
+ this.#setState({ error: fadeResult.error });
1465
+ }
1466
+ return OK_RESULT;
1467
+ }
1468
+ async #pause(options = {}) {
1469
+ if (this.#destroyed) {
1470
+ return;
1471
+ }
1472
+ const version = this.#beginCommand("paused");
1473
+ const config = this.#state.experienceConfig;
1474
+ await this.#halt(version, {
1475
+ fadeDurationMs: options.fade === false ? 0 : config.pauseFadeOutMs,
1476
+ fadeCurve: config.fadeCurve,
1477
+ resetPosition: false,
1478
+ transitionCause: "user-pause"
1479
+ });
1480
+ }
1481
+ async #stop(options = {}) {
1482
+ if (this.#destroyed) {
1483
+ return;
1484
+ }
1485
+ const version = this.#beginCommand("paused");
1486
+ const config = this.#state.experienceConfig;
1487
+ await this.#halt(version, {
1488
+ fadeDurationMs: options.fade === false ? 0 : config.pauseFadeOutMs,
1489
+ fadeCurve: config.fadeCurve,
1490
+ resetPosition: options.resetPosition ?? true,
1491
+ transitionCause: "user-stop"
1492
+ });
1493
+ }
1494
+ async #halt(version, options) {
1495
+ void this.#savePodcastProgress(true);
1496
+ const fadeResult = await runFade(this.#gain, {
1497
+ target: 0,
1498
+ durationMs: options.fadeDurationMs,
1499
+ curve: options.fadeCurve
1500
+ });
1501
+ if (!this.#isCurrentCommand(version)) {
1502
+ return false;
1503
+ }
1504
+ if (fadeResult.error !== null) {
1505
+ this.#setState({ error: fadeResult.error });
1506
+ }
1507
+ this.#pauseMediaElement();
1508
+ if (options.resetPosition && this.#state.currentItem !== null) {
1509
+ this.#media.seekTo(0);
1510
+ }
1511
+ this.#setState({
1512
+ status: this.#state.currentItem === null ? "idle" : "paused",
1513
+ currentTimeSec: options.resetPosition ? 0 : readFiniteCurrentTime(this.#media),
1514
+ ...options.transitionCause === void 0 ? {} : { lastTransitionCause: options.transitionCause }
1515
+ });
1516
+ return true;
1517
+ }
1518
+ async #togglePlayback() {
1519
+ if (this.#state.status === "playing") {
1520
+ await this.#pause();
1521
+ return OK_RESULT;
1522
+ }
1523
+ return this.#play();
1524
+ }
1525
+ #seekTo(seconds) {
1526
+ if (this.#destroyed || this.#state.currentItem === null || !Number.isFinite(seconds)) {
1527
+ return;
1528
+ }
1529
+ const duration = readFiniteDuration(this.#media);
1530
+ const lowerBounded = Math.max(seconds, 0);
1531
+ const target = duration === null ? lowerBounded : Math.min(lowerBounded, duration);
1532
+ const version = this.#beginCommand(this.#desiredPlayback);
1533
+ const config = this.#state.experienceConfig;
1534
+ void this.#performSeek(
1535
+ version,
1536
+ target,
1537
+ config.seekFadeMs,
1538
+ config.fadeCurve
1539
+ );
1540
+ }
1541
+ #seekBy(deltaSeconds) {
1542
+ if (!Number.isFinite(deltaSeconds)) {
1543
+ return;
1544
+ }
1545
+ this.#seekTo(readFiniteCurrentTime(this.#media) + deltaSeconds);
1546
+ }
1547
+ async #performSeek(version, target, fadeDurationMs, fadeCurve) {
1548
+ const fadeOut = await runFade(this.#gain, {
1549
+ target: 0,
1550
+ durationMs: fadeDurationMs,
1551
+ curve: fadeCurve
1552
+ });
1553
+ if (!this.#isCurrentCommand(version)) {
1554
+ return;
1555
+ }
1556
+ if (fadeOut.error !== null) {
1557
+ this.#setState({ error: fadeOut.error });
1558
+ }
1559
+ this.#setState({ status: "seeking" });
1560
+ this.#media.seekTo(target);
1561
+ this.#setState({ currentTimeSec: target });
1562
+ const fadeIn = await runFade(this.#gain, {
1563
+ target: 1,
1564
+ durationMs: fadeDurationMs,
1565
+ curve: fadeCurve
1566
+ });
1567
+ if (!this.#isCurrentCommand(version)) {
1568
+ return;
1569
+ }
1570
+ if (fadeIn.error !== null) {
1571
+ this.#setState({ error: fadeIn.error });
1572
+ }
1573
+ this.#setState({
1574
+ status: this.#media.isPaused() ? "paused" : "playing"
1575
+ });
1576
+ }
1577
+ #setVolume(volume) {
1578
+ if (this.#destroyed) {
1579
+ return;
1580
+ }
1581
+ const normalized = clampVolume2(volume);
1582
+ this.#media.setVolume(normalized);
1583
+ this.#setState({ volume: normalized });
1584
+ }
1585
+ #toggleMute() {
1586
+ if (this.#destroyed) {
1587
+ return;
1588
+ }
1589
+ const muted = !this.#state.muted;
1590
+ this.#media.setMuted(muted);
1591
+ this.#setState({ muted });
1592
+ }
1593
+ #setShuffle(enabled) {
1594
+ if (this.#destroyed || enabled === this.#state.shuffleEnabled) {
1595
+ return;
1596
+ }
1597
+ const currentQueueItemId = this.#state.currentQueueItemId;
1598
+ if (currentQueueItemId === null) {
1599
+ this.#setState({ shuffleEnabled: enabled });
1600
+ return;
1601
+ }
1602
+ const playbackQueue = enabled ? shuffleQueueAroundCurrent(
1603
+ this.#state.queue,
1604
+ currentQueueItemId,
1605
+ this.#random
1606
+ ) : this.#state.queue;
1607
+ this.#playbackOrderQueueItemIds = Object.freeze(
1608
+ playbackQueue.map((entry) => entry.queueItemId)
1609
+ );
1610
+ this.#playbackOrderCursor = enabled ? 0 : findQueueIndex(this.#state.queue, currentQueueItemId) ?? -1;
1611
+ this.#setState({
1612
+ shuffleEnabled: enabled
1613
+ });
1614
+ }
1615
+ #setRepeatMode(mode) {
1616
+ if (this.#destroyed) {
1617
+ return;
1618
+ }
1619
+ this.#setState({ repeatMode: mode });
1620
+ }
1621
+ async #next() {
1622
+ if (this.#destroyed) {
1623
+ return failed(DESTROYED_ERROR);
1624
+ }
1625
+ const currentQueueItemId = this.#state.currentQueueItemId;
1626
+ if (currentQueueItemId === null) {
1627
+ return failed(NO_ITEM_ERROR);
1628
+ }
1629
+ const nextPlaybackIndex = this.#getNextPlaybackIndex(
1630
+ currentQueueItemId
1631
+ );
1632
+ if (nextPlaybackIndex === null) {
1633
+ return OK_RESULT;
1634
+ }
1635
+ const nextQueueItemId = this.#playbackOrderQueueItemIds[nextPlaybackIndex];
1636
+ const nextIndex = nextQueueItemId === void 0 ? null : findQueueIndex(this.#state.queue, nextQueueItemId);
1637
+ if (nextIndex === null) {
1638
+ return failed(INVALID_QUEUE_ERROR);
1639
+ }
1640
+ this.#playbackOrderCursor = nextPlaybackIndex;
1641
+ const autoplay = this.#state.status === "playing";
1642
+ const version = this.#beginCommand(
1643
+ autoplay ? "playing" : "paused"
1644
+ );
1645
+ return this.#selectQueueIndex(version, nextIndex, autoplay);
1646
+ }
1647
+ async #previous() {
1648
+ if (this.#destroyed) {
1649
+ return failed(DESTROYED_ERROR);
1650
+ }
1651
+ const currentIndex = this.#state.currentQueueIndex;
1652
+ const currentItem = this.#state.currentItem;
1653
+ if (currentIndex === null || currentItem === null) {
1654
+ return failed(NO_ITEM_ERROR);
1655
+ }
1656
+ const config = this.#state.experienceConfig;
1657
+ const currentTime = readFiniteCurrentTime(this.#media);
1658
+ const shouldRestartTrack = currentItem.kind === "track" && config.previousRestartThresholdSec > 0 && currentTime >= config.previousRestartThresholdSec;
1659
+ const shouldRestartPodcast = currentItem.kind === "podcastEpisode" && config.podcastPreviousMode === "restart-episode";
1660
+ const previousPlaybackIndex = getPreviousQueueIndex(
1661
+ this.#playbackOrderQueueItemIds,
1662
+ this.#playbackOrderCursor
1663
+ );
1664
+ const previousQueueItemId = previousPlaybackIndex === null ? void 0 : this.#playbackOrderQueueItemIds[previousPlaybackIndex];
1665
+ const previousIndex = previousQueueItemId === void 0 ? null : findQueueIndex(this.#state.queue, previousQueueItemId);
1666
+ if (shouldRestartTrack || shouldRestartPodcast || previousPlaybackIndex === null || previousIndex === null) {
1667
+ this.#seekTo(0);
1668
+ return OK_RESULT;
1669
+ }
1670
+ this.#playbackOrderCursor = previousPlaybackIndex;
1671
+ const autoplay = this.#state.status === "playing";
1672
+ const version = this.#beginCommand(
1673
+ autoplay ? "playing" : "paused"
1674
+ );
1675
+ return this.#selectQueueIndex(version, previousIndex, autoplay);
1676
+ }
1677
+ async #retry() {
1678
+ if (this.#destroyed) {
1679
+ return failed(DESTROYED_ERROR);
1680
+ }
1681
+ if (this.#state.currentQueueIndex === null || this.#state.error === null || !this.#state.error.recoverable) {
1682
+ return failed(this.#state.error ?? NO_ITEM_ERROR);
1683
+ }
1684
+ const version = this.#beginCommand("playing");
1685
+ this.#setState({ error: null, needsUserGesture: false });
1686
+ return this.#selectQueueIndex(
1687
+ version,
1688
+ this.#state.currentQueueIndex,
1689
+ true
1690
+ );
1691
+ }
1692
+ async #clear() {
1693
+ if (this.#destroyed) {
1694
+ return;
1695
+ }
1696
+ const version = this.#beginCommand("paused");
1697
+ const config = this.#state.experienceConfig;
1698
+ const halted = await this.#halt(version, {
1699
+ fadeDurationMs: config.pauseFadeOutMs,
1700
+ fadeCurve: config.fadeCurve,
1701
+ resetPosition: true
1702
+ });
1703
+ if (!halted || !this.#isCurrentCommand(version)) {
1704
+ return;
1705
+ }
1706
+ this.#media.clearSource();
1707
+ this.#playbackOrderQueueItemIds = Object.freeze([]);
1708
+ this.#playbackOrderCursor = -1;
1709
+ this.#setState({
1710
+ currentItem: null,
1711
+ currentItemId: null,
1712
+ currentQueueItemId: null,
1713
+ mediaKind: null,
1714
+ status: "idle",
1715
+ currentTimeSec: 0,
1716
+ durationSec: null,
1717
+ queue: Object.freeze([]),
1718
+ canSeek: false,
1719
+ needsUserGesture: false,
1720
+ error: null
1721
+ });
1722
+ }
1723
+ #restoreGain() {
1724
+ try {
1725
+ this.#gain?.setValue(1);
1726
+ } catch {
1727
+ this.#setState({ error: createPlayerError("audio-context", true) });
1728
+ }
1729
+ }
1730
+ async #savePodcastProgress(force) {
1731
+ const item = this.#state.currentItem;
1732
+ const port = this.#podcastProgress;
1733
+ if (port === void 0 || !canUsePodcastProgress(
1734
+ item,
1735
+ port,
1736
+ this.#state.experienceConfig.persistMode
1737
+ )) {
1738
+ return;
1739
+ }
1740
+ const position = readFiniteCurrentTime(this.#media);
1741
+ const lastPosition = this.#lastSavedPodcastPositions.get(item.id);
1742
+ if (!force && lastPosition !== void 0 && Math.abs(position - lastPosition) < this.#podcastProgressSaveIntervalSec) {
1743
+ return;
1744
+ }
1745
+ const progress = createPodcastProgress(item, position, this.#now);
1746
+ this.#lastSavedPodcastPositions.set(item.id, position);
1747
+ this.#progressSaveChain = this.#progressSaveChain.then(() => port.save(progress)).catch(() => void 0);
1748
+ await this.#progressSaveChain;
1749
+ }
1750
+ #handleMediaEvent(event) {
1751
+ if (this.#destroyed) {
1752
+ return;
1753
+ }
1754
+ const snapshot = this.#media.getStateSnapshot();
1755
+ if (this.#state.currentItem !== null && (!snapshot.hasCurrentSource || !snapshot.currentSourceMatchesAssigned)) {
1756
+ this.#desiredPlayback = "paused";
1757
+ this.#setState({
1758
+ status: "error",
1759
+ lastTransitionCause: "source-error",
1760
+ error: createPlayerError("unknown", true)
1761
+ });
1762
+ return;
1763
+ }
1764
+ this.#setState({
1765
+ currentTimeSec: snapshot.currentTimeSec,
1766
+ durationSec: snapshot.durationSec,
1767
+ volume: clampVolume2(snapshot.volume),
1768
+ muted: snapshot.muted,
1769
+ canSeek: snapshot.durationSec !== null
1770
+ });
1771
+ switch (event.type) {
1772
+ case "loadstart":
1773
+ this.#setState({ status: "loading" });
1774
+ break;
1775
+ case "loadedmetadata":
1776
+ case "durationchange": {
1777
+ const duration = readFiniteDuration(this.#media);
1778
+ this.#setState({
1779
+ durationSec: duration,
1780
+ canSeek: duration !== null
1781
+ });
1782
+ break;
1783
+ }
1784
+ case "canplay":
1785
+ this.#setState({
1786
+ status: this.#media.isPaused() ? "paused" : "playing"
1787
+ });
1788
+ break;
1789
+ case "play":
1790
+ case "playing":
1791
+ if (this.#desiredPlayback === "paused") {
1792
+ this.#pauseMediaElement();
1793
+ break;
1794
+ }
1795
+ this.#setState({
1796
+ status: "playing",
1797
+ needsUserGesture: false,
1798
+ error: null
1799
+ });
1800
+ break;
1801
+ case "pause":
1802
+ if (this.#ignoreNextPauseEvent) {
1803
+ this.#ignoreNextPauseEvent = false;
1804
+ break;
1805
+ }
1806
+ if (this.#desiredPlayback === "playing") {
1807
+ this.#desiredPlayback = "paused";
1808
+ }
1809
+ if (this.#state.status !== "ended") {
1810
+ this.#setState({
1811
+ status: this.#state.currentItem === null ? "idle" : "paused",
1812
+ ...this.#state.status === "playing" ? { lastTransitionCause: "os-interruption" } : {}
1813
+ });
1814
+ }
1815
+ break;
1816
+ case "waiting":
1817
+ case "stalled":
1818
+ this.#setState({ status: "loading" });
1819
+ break;
1820
+ case "seeking":
1821
+ this.#setState({ status: "seeking" });
1822
+ break;
1823
+ case "seeked":
1824
+ this.#setState({
1825
+ currentTimeSec: readFiniteCurrentTime(this.#media),
1826
+ status: this.#media.isPaused() ? "paused" : "playing"
1827
+ });
1828
+ break;
1829
+ case "timeupdate":
1830
+ this.#setState({
1831
+ currentTimeSec: readFiniteCurrentTime(this.#media),
1832
+ ...this.#state.status === "loading" && !snapshot.paused ? { status: "playing" } : {}
1833
+ });
1834
+ void this.#savePodcastProgress(false);
1835
+ break;
1836
+ case "ended":
1837
+ void this.#handleEnded();
1838
+ break;
1839
+ case "error": {
1840
+ const error = this.#media.getError() ?? createPlayerError("unknown", true);
1841
+ this.#desiredPlayback = "paused";
1842
+ this.#setState({
1843
+ status: "error",
1844
+ lastTransitionCause: "source-error",
1845
+ error
1846
+ });
1847
+ break;
1848
+ }
1849
+ case "volumechange":
1850
+ this.#setState({
1851
+ volume: clampVolume2(this.#media.getVolume()),
1852
+ muted: this.#media.isMuted()
1853
+ });
1854
+ break;
1855
+ }
1856
+ }
1857
+ async #handleEnded() {
1858
+ if (this.#handlingEnded) {
1859
+ return;
1860
+ }
1861
+ this.#handlingEnded = true;
1862
+ try {
1863
+ await this.#advanceAfterEnded();
1864
+ } finally {
1865
+ this.#handlingEnded = false;
1866
+ }
1867
+ }
1868
+ async #advanceAfterEnded() {
1869
+ this.#setState({ lastTransitionCause: "natural-end" });
1870
+ void this.#savePodcastProgress(true);
1871
+ const currentQueueItemId = this.#state.currentQueueItemId;
1872
+ if (currentQueueItemId === null) {
1873
+ return;
1874
+ }
1875
+ if (this.#state.repeatMode === "one") {
1876
+ const version2 = this.#beginCommand("playing");
1877
+ const config = this.#state.experienceConfig;
1878
+ this.#media.seekTo(0);
1879
+ this.#setState({ currentTimeSec: 0, status: "loading" });
1880
+ await this.#playWithCommand(
1881
+ version2,
1882
+ config.trackChangeFadeInMs,
1883
+ config.fadeCurve
1884
+ );
1885
+ return;
1886
+ }
1887
+ const nextPlaybackIndex = this.#getNextPlaybackIndex(
1888
+ currentQueueItemId
1889
+ );
1890
+ if (nextPlaybackIndex === null) {
1891
+ this.#desiredPlayback = "paused";
1892
+ this.#setState({
1893
+ status: "ended",
1894
+ currentTimeSec: readFiniteDuration(this.#media) ?? readFiniteCurrentTime(this.#media)
1895
+ });
1896
+ return;
1897
+ }
1898
+ const nextQueueItemId = this.#playbackOrderQueueItemIds[nextPlaybackIndex];
1899
+ const nextIndex = nextQueueItemId === void 0 ? null : findQueueIndex(this.#state.queue, nextQueueItemId);
1900
+ if (nextIndex === null) {
1901
+ this.#desiredPlayback = "paused";
1902
+ this.#setState({
1903
+ status: "error",
1904
+ error: INVALID_QUEUE_ERROR
1905
+ });
1906
+ return;
1907
+ }
1908
+ this.#playbackOrderCursor = nextPlaybackIndex;
1909
+ const version = this.#beginCommand("playing");
1910
+ await this.#selectQueueIndex(version, nextIndex, true);
1911
+ }
1912
+ #getNextPlaybackIndex(currentQueueItemId) {
1913
+ if (this.#playbackOrderCursor >= 0 && this.#playbackOrderCursor < this.#playbackOrderQueueItemIds.length - 1) {
1914
+ return this.#playbackOrderCursor + 1;
1915
+ }
1916
+ if (!this.#state.shuffleEnabled) {
1917
+ return getNextQueueIndex(
1918
+ this.#playbackOrderQueueItemIds,
1919
+ this.#playbackOrderCursor,
1920
+ this.#state.repeatMode
1921
+ );
1922
+ }
1923
+ if (this.#state.repeatMode !== "all") {
1924
+ return null;
1925
+ }
1926
+ const shuffled = shuffleQueueAroundCurrent(
1927
+ this.#state.queue,
1928
+ currentQueueItemId,
1929
+ this.#random
1930
+ );
1931
+ const continuation = shuffled.slice(1).map((entry) => entry.queueItemId);
1932
+ if (continuation.length === 0) {
1933
+ if (this.#state.queue.length !== 1) {
1934
+ return null;
1935
+ }
1936
+ continuation.push(currentQueueItemId);
1937
+ }
1938
+ this.#playbackOrderQueueItemIds = Object.freeze([
1939
+ ...this.#playbackOrderQueueItemIds,
1940
+ ...continuation
1941
+ ]);
1942
+ let appendedIndex = this.#playbackOrderCursor + 1;
1943
+ if (this.#playbackOrderQueueItemIds.length > MAX_SHUFFLE_HISTORY_ITEMS && this.#playbackOrderCursor > RETAINED_SHUFFLE_HISTORY_ITEMS) {
1944
+ const removeCount = this.#playbackOrderCursor - RETAINED_SHUFFLE_HISTORY_ITEMS;
1945
+ this.#playbackOrderQueueItemIds = Object.freeze(
1946
+ this.#playbackOrderQueueItemIds.slice(removeCount)
1947
+ );
1948
+ this.#playbackOrderCursor -= removeCount;
1949
+ appendedIndex -= removeCount;
1950
+ }
1951
+ return appendedIndex;
1952
+ }
1953
+ };
1954
+ function createPlayerStore(options) {
1955
+ return new CorePlayerStore(options);
1956
+ }
1957
+
1958
+ // src/react/runtime-commands.ts
1959
+ function activate(gain, options) {
1960
+ options.onUserActivation?.();
1961
+ try {
1962
+ const activation = gain.activateFromUserGesture();
1963
+ if (activation !== void 0) {
1964
+ void activation.catch(() => {
1965
+ options.onActivationError?.();
1966
+ });
1967
+ }
1968
+ } catch {
1969
+ options.onActivationError?.();
1970
+ }
1971
+ }
1972
+ function reportCommand(options, command) {
1973
+ options.onCommand?.(command);
1974
+ }
1975
+ function createGestureAwareCommands(commands, gain, activationOptions = {}) {
1976
+ return Object.freeze({
1977
+ load(item) {
1978
+ reportCommand(activationOptions, "load");
1979
+ return commands.load(item);
1980
+ },
1981
+ setQueue(queue, commandOptions) {
1982
+ reportCommand(activationOptions, "setQueue");
1983
+ if (commandOptions?.autoplay === true) {
1984
+ activate(gain, activationOptions);
1985
+ }
1986
+ return commands.setQueue(queue, commandOptions);
1987
+ },
1988
+ play() {
1989
+ reportCommand(activationOptions, "play");
1990
+ activate(gain, activationOptions);
1991
+ return commands.play();
1992
+ },
1993
+ pause(options) {
1994
+ reportCommand(activationOptions, "pause");
1995
+ return commands.pause(options);
1996
+ },
1997
+ stop(options) {
1998
+ reportCommand(activationOptions, "stop");
1999
+ return commands.stop(options);
2000
+ },
2001
+ togglePlayback() {
2002
+ reportCommand(activationOptions, "togglePlayback");
2003
+ activate(gain, activationOptions);
2004
+ return commands.togglePlayback();
2005
+ },
2006
+ seekTo(seconds) {
2007
+ reportCommand(activationOptions, "seekTo");
2008
+ activate(gain, activationOptions);
2009
+ commands.seekTo(seconds);
2010
+ },
2011
+ seekBy(deltaSeconds) {
2012
+ reportCommand(activationOptions, "seekBy");
2013
+ activate(gain, activationOptions);
2014
+ commands.seekBy(deltaSeconds);
2015
+ },
2016
+ setVolume(volume) {
2017
+ reportCommand(activationOptions, "setVolume");
2018
+ commands.setVolume(volume);
2019
+ },
2020
+ toggleMute() {
2021
+ reportCommand(activationOptions, "toggleMute");
2022
+ commands.toggleMute();
2023
+ },
2024
+ setShuffle(enabled) {
2025
+ reportCommand(activationOptions, "setShuffle");
2026
+ commands.setShuffle(enabled);
2027
+ },
2028
+ setRepeatMode(mode) {
2029
+ reportCommand(activationOptions, "setRepeatMode");
2030
+ commands.setRepeatMode(mode);
2031
+ },
2032
+ next() {
2033
+ reportCommand(activationOptions, "next");
2034
+ activate(gain, activationOptions);
2035
+ return commands.next();
2036
+ },
2037
+ previous() {
2038
+ reportCommand(activationOptions, "previous");
2039
+ activate(gain, activationOptions);
2040
+ return commands.previous();
2041
+ },
2042
+ retry() {
2043
+ reportCommand(activationOptions, "retry");
2044
+ activate(gain, activationOptions);
2045
+ return commands.retry();
2046
+ },
2047
+ clear() {
2048
+ reportCommand(activationOptions, "clear");
2049
+ return commands.clear();
2050
+ }
2051
+ });
2052
+ }
2053
+ var DIAGNOSTIC_MEDIA_EVENTS = [
2054
+ "loadstart",
2055
+ "loadedmetadata",
2056
+ "canplay",
2057
+ "play",
2058
+ "playing",
2059
+ "pause",
2060
+ "waiting",
2061
+ "stalled",
2062
+ "seeking",
2063
+ "seeked",
2064
+ "timeupdate",
2065
+ "ended",
2066
+ "error",
2067
+ "durationchange",
2068
+ "volumechange"
2069
+ ];
2070
+ var EMPTY_INITIAL_QUEUE = Object.freeze([]);
2071
+ var PlayerRuntimeContext = createContext(null);
2072
+ function PlayerProvider({
2073
+ audioMode = "auto",
2074
+ children,
2075
+ experienceConfig,
2076
+ initialQueue = EMPTY_INITIAL_QUEUE,
2077
+ initialQueueItemId,
2078
+ loadingFallback = null,
2079
+ onDiagnosticEvent
2080
+ }) {
2081
+ const audioRef = useRef(null);
2082
+ const initialQueueRef = useRef(initialQueue);
2083
+ const initialQueueItemIdRef = useRef(initialQueueItemId);
2084
+ const initialExperienceConfigRef = useRef(experienceConfig);
2085
+ const diagnosticListenerRef = useRef(onDiagnosticEvent);
2086
+ const [runtime, setRuntime] = useState(null);
2087
+ const [audioContextState, setAudioContextState] = useState("uninitialized");
2088
+ const [resolvedAudioMode, setResolvedAudioMode] = useState("web-audio");
2089
+ const [mediaSessionSupported, setMediaSessionSupported] = useState(false);
2090
+ const [
2091
+ mediaSessionActionsEnabled,
2092
+ setMediaSessionActionsEnabled
2093
+ ] = useState(false);
2094
+ useEffect(() => {
2095
+ diagnosticListenerRef.current = onDiagnosticEvent;
2096
+ }, [onDiagnosticEvent]);
2097
+ useEffect(() => {
2098
+ const audioElement = audioRef.current;
2099
+ if (audioElement === null) {
2100
+ return;
2101
+ }
2102
+ let active = true;
2103
+ let synchronizeFromLifecycle = null;
2104
+ const emitDiagnostic = (event) => {
2105
+ diagnosticListenerRef.current?.({
2106
+ ...event,
2107
+ occurredAt: (/* @__PURE__ */ new Date()).toISOString()
2108
+ });
2109
+ };
2110
+ const media = new BrowserMediaElementPort(audioElement);
2111
+ const navigatorPlatform = Reflect.get(
2112
+ navigator,
2113
+ "platform"
2114
+ );
2115
+ const nextAudioMode = resolvePlayerAudioMode(audioMode, {
2116
+ userAgent: navigator.userAgent,
2117
+ ...typeof navigatorPlatform === "string" ? { platform: navigatorPlatform } : {},
2118
+ maxTouchPoints: navigator.maxTouchPoints
2119
+ });
2120
+ const gain = nextAudioMode === "web-audio" ? new LazyWebAudioGainPort(audioElement, {
2121
+ onContextStateChange: (state) => {
2122
+ if (active) {
2123
+ setAudioContextState(state);
2124
+ emitDiagnostic({
2125
+ type: "audio-context-statechange",
2126
+ state
2127
+ });
2128
+ synchronizeFromLifecycle?.(
2129
+ "audio-context-statechange"
2130
+ );
2131
+ }
2132
+ }
2133
+ }) : new HtmlMediaElementGainPort();
2134
+ setResolvedAudioMode(nextAudioMode);
2135
+ setAudioContextState(
2136
+ gain instanceof LazyWebAudioGainPort ? gain.getContextState() : "uninitialized"
2137
+ );
2138
+ const store = createPlayerStore({
2139
+ gain,
2140
+ media,
2141
+ ...initialExperienceConfigRef.current === void 0 ? {} : { experienceConfig: initialExperienceConfigRef.current }
2142
+ });
2143
+ let mediaSessionController = null;
2144
+ const commands = createGestureAwareCommands(store.commands, gain, {
2145
+ onUserActivation: () => {
2146
+ mediaSessionController?.enableActionHandlers();
2147
+ if (active) {
2148
+ setMediaSessionActionsEnabled(
2149
+ mediaSessionController?.actionsEnabled ?? false
2150
+ );
2151
+ }
2152
+ },
2153
+ onCommand: (command) => {
2154
+ emitDiagnostic({ type: "command", command });
2155
+ },
2156
+ onActivationError: () => {
2157
+ emitDiagnostic({
2158
+ type: "audio-context-resume",
2159
+ outcome: "failed",
2160
+ state: gain instanceof LazyWebAudioGainPort ? gain.getContextState() : "uninitialized",
2161
+ errorCode: "audio-context"
2162
+ });
2163
+ }
2164
+ });
2165
+ mediaSessionController = createBrowserMediaSessionController({
2166
+ commands,
2167
+ getState: () => store.getState(),
2168
+ onAction: (action) => {
2169
+ emitDiagnostic({
2170
+ type: "media-session-action",
2171
+ action,
2172
+ failed: false
2173
+ });
2174
+ },
2175
+ onActionError: (action) => {
2176
+ emitDiagnostic({
2177
+ type: "media-session-action",
2178
+ action,
2179
+ failed: true
2180
+ });
2181
+ }
2182
+ });
2183
+ const activeMediaSessionController = mediaSessionController;
2184
+ setMediaSessionSupported(activeMediaSessionController.supported);
2185
+ setMediaSessionActionsEnabled(false);
2186
+ const unsubscribeMediaSession = store.subscribe(() => {
2187
+ activeMediaSessionController.update(store.getState());
2188
+ });
2189
+ activeMediaSessionController.update(store.getState());
2190
+ const synchronize = (reason) => {
2191
+ const resumeAudioContext = async () => {
2192
+ if (!(gain instanceof LazyWebAudioGainPort) || gain.getContextState() !== "suspended") {
2193
+ return;
2194
+ }
2195
+ try {
2196
+ const resumed = await gain.resumeIfSuspended();
2197
+ if (resumed) {
2198
+ emitDiagnostic({
2199
+ type: "audio-context-resume",
2200
+ outcome: "resumed",
2201
+ state: gain.getContextState(),
2202
+ errorCode: null
2203
+ });
2204
+ }
2205
+ } catch {
2206
+ emitDiagnostic({
2207
+ type: "audio-context-resume",
2208
+ outcome: "failed",
2209
+ state: gain.getContextState(),
2210
+ errorCode: "audio-context"
2211
+ });
2212
+ }
2213
+ };
2214
+ void resumeAudioContext().then(() => store.synchronizeFromMedia(reason)).finally(() => {
2215
+ if (!active) {
2216
+ return;
2217
+ }
2218
+ if (gain instanceof LazyWebAudioGainPort) {
2219
+ setAudioContextState(gain.getContextState());
2220
+ }
2221
+ activeMediaSessionController.update(store.getState());
2222
+ emitDiagnostic({
2223
+ type: "lifecycle-sync",
2224
+ reason,
2225
+ visibility: document.visibilityState,
2226
+ media: media.getStateSnapshot()
2227
+ });
2228
+ });
2229
+ };
2230
+ synchronizeFromLifecycle = synchronize;
2231
+ const handleVisibilityChange = () => {
2232
+ if (document.visibilityState === "visible") {
2233
+ synchronize("visibilitychange");
2234
+ }
2235
+ };
2236
+ const handlePageShow = () => {
2237
+ synchronize("pageshow");
2238
+ };
2239
+ const handleFocus = () => {
2240
+ synchronize("focus");
2241
+ };
2242
+ const mediaEventListeners = /* @__PURE__ */ new Map();
2243
+ for (const eventType of DIAGNOSTIC_MEDIA_EVENTS) {
2244
+ const listener = () => {
2245
+ emitDiagnostic({
2246
+ type: "media-event",
2247
+ event: eventType,
2248
+ media: media.getStateSnapshot()
2249
+ });
2250
+ activeMediaSessionController.update(store.getState());
2251
+ };
2252
+ mediaEventListeners.set(eventType, listener);
2253
+ media.addEventListener(eventType, listener);
2254
+ }
2255
+ document.addEventListener(
2256
+ "visibilitychange",
2257
+ handleVisibilityChange
2258
+ );
2259
+ window.addEventListener("pageshow", handlePageShow);
2260
+ window.addEventListener("focus", handleFocus);
2261
+ const contextValue2 = {
2262
+ store,
2263
+ commands,
2264
+ experience: store.experience,
2265
+ audioContextState: "uninitialized",
2266
+ audioMode: nextAudioMode,
2267
+ mediaSessionSupported: activeMediaSessionController.supported,
2268
+ mediaSessionActionsEnabled: false
2269
+ };
2270
+ setRuntime(contextValue2);
2271
+ void store.commands.setQueue(initialQueueRef.current, {
2272
+ ...initialQueueItemIdRef.current === void 0 ? {} : { startQueueItemId: initialQueueItemIdRef.current },
2273
+ autoplay: false
2274
+ });
2275
+ return () => {
2276
+ active = false;
2277
+ synchronizeFromLifecycle = null;
2278
+ document.removeEventListener(
2279
+ "visibilitychange",
2280
+ handleVisibilityChange
2281
+ );
2282
+ window.removeEventListener("pageshow", handlePageShow);
2283
+ window.removeEventListener("focus", handleFocus);
2284
+ for (const [eventType, listener] of mediaEventListeners) {
2285
+ media.removeEventListener(eventType, listener);
2286
+ }
2287
+ unsubscribeMediaSession();
2288
+ activeMediaSessionController.destroy();
2289
+ setRuntime(
2290
+ (current) => current?.store === store ? null : current
2291
+ );
2292
+ void store.destroy();
2293
+ };
2294
+ }, [
2295
+ audioMode
2296
+ ]);
2297
+ const contextValue = runtime === null ? null : {
2298
+ ...runtime,
2299
+ audioContextState,
2300
+ audioMode: resolvedAudioMode,
2301
+ mediaSessionSupported,
2302
+ mediaSessionActionsEnabled
2303
+ };
2304
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
2305
+ /* @__PURE__ */ jsx(
2306
+ "audio",
2307
+ {
2308
+ "aria-hidden": "true",
2309
+ "data-audio-context-state": audioContextState,
2310
+ "data-audio-mode": resolvedAudioMode,
2311
+ "data-media-session-actions": mediaSessionActionsEnabled ? "enabled" : "inactive",
2312
+ "data-media-session-supported": mediaSessionSupported ? "true" : "false",
2313
+ "data-player-audio": "true",
2314
+ preload: "metadata",
2315
+ ref: audioRef
2316
+ }
2317
+ ),
2318
+ contextValue === null ? loadingFallback : /* @__PURE__ */ jsx(PlayerRuntimeContext.Provider, { value: contextValue, children })
2319
+ ] });
2320
+ }
2321
+ function MiniPlayerHost({
2322
+ children,
2323
+ className,
2324
+ onInsetChange,
2325
+ visible
2326
+ }) {
2327
+ const hostRef = useRef(null);
2328
+ useEffect(() => {
2329
+ if (!visible) {
2330
+ onInsetChange?.(0);
2331
+ return;
2332
+ }
2333
+ const host = hostRef.current;
2334
+ if (host === null) {
2335
+ return;
2336
+ }
2337
+ const publishInset = () => {
2338
+ onInsetChange?.(Math.ceil(host.getBoundingClientRect().height));
2339
+ };
2340
+ publishInset();
2341
+ if (typeof ResizeObserver === "undefined") {
2342
+ return () => {
2343
+ onInsetChange?.(0);
2344
+ };
2345
+ }
2346
+ const observer = new ResizeObserver(publishInset);
2347
+ observer.observe(host);
2348
+ return () => {
2349
+ observer.disconnect();
2350
+ onInsetChange?.(0);
2351
+ };
2352
+ }, [onInsetChange, visible]);
2353
+ if (!visible) {
2354
+ return null;
2355
+ }
2356
+ return /* @__PURE__ */ jsx(
2357
+ "div",
2358
+ {
2359
+ className,
2360
+ "data-player-mini-host": "true",
2361
+ ref: hostRef,
2362
+ children
2363
+ }
2364
+ );
2365
+ }
2366
+ function usePlayerRuntimeContext() {
2367
+ const context = useContext(PlayerRuntimeContext);
2368
+ if (context === null) {
2369
+ throw new Error("usePlayer must be used inside PlayerProvider.");
2370
+ }
2371
+ return context;
2372
+ }
2373
+ function usePlayer() {
2374
+ const context = usePlayerRuntimeContext();
2375
+ const state = useSyncExternalStore(
2376
+ (listener) => context.store.subscribe(listener),
2377
+ () => context.store.getState(),
2378
+ () => context.store.getState()
2379
+ );
2380
+ return {
2381
+ state,
2382
+ commands: context.commands,
2383
+ experience: context.experience,
2384
+ audioContextState: context.audioContextState,
2385
+ audioMode: context.audioMode,
2386
+ mediaSessionSupported: context.mediaSessionSupported,
2387
+ mediaSessionActionsEnabled: context.mediaSessionActionsEnabled
2388
+ };
2389
+ }
2390
+
2391
+ export { MiniPlayerHost, PlayerProvider, usePlayer };
2392
+ //# sourceMappingURL=index.js.map
2393
+ //# sourceMappingURL=index.js.map