@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.
- package/README.md +159 -0
- package/dist/config/index.d.ts +37 -0
- package/dist/config/index.js +280 -0
- package/dist/config/index.js.map +1 -0
- package/dist/consumer/index.d.ts +27 -0
- package/dist/consumer/index.js +38 -0
- package/dist/consumer/index.js.map +1 -0
- package/dist/consumer-smoke/node_modules/.modules.yaml +30 -0
- package/dist/consumer-smoke/node_modules/.package-map.json +1 -0
- package/dist/consumer-smoke/node_modules/.pnpm/lock.yaml +57 -0
- package/dist/consumer-smoke/node_modules/.pnpm/scheduler@0.27.0/node_modules/scheduler/LICENSE +21 -0
- package/dist/consumer-smoke/node_modules/.pnpm/scheduler@0.27.0/node_modules/scheduler/README.md +9 -0
- package/dist/consumer-smoke/node_modules/.pnpm/scheduler@0.27.0/node_modules/scheduler/cjs/scheduler-unstable_mock.development.js +414 -0
- package/dist/consumer-smoke/node_modules/.pnpm/scheduler@0.27.0/node_modules/scheduler/cjs/scheduler-unstable_mock.production.js +406 -0
- package/dist/consumer-smoke/node_modules/.pnpm/scheduler@0.27.0/node_modules/scheduler/cjs/scheduler-unstable_post_task.development.js +150 -0
- package/dist/consumer-smoke/node_modules/.pnpm/scheduler@0.27.0/node_modules/scheduler/cjs/scheduler-unstable_post_task.production.js +140 -0
- package/dist/consumer-smoke/node_modules/.pnpm/scheduler@0.27.0/node_modules/scheduler/cjs/scheduler.development.js +364 -0
- package/dist/consumer-smoke/node_modules/.pnpm/scheduler@0.27.0/node_modules/scheduler/cjs/scheduler.native.development.js +350 -0
- package/dist/consumer-smoke/node_modules/.pnpm/scheduler@0.27.0/node_modules/scheduler/cjs/scheduler.native.production.js +330 -0
- package/dist/consumer-smoke/node_modules/.pnpm/scheduler@0.27.0/node_modules/scheduler/cjs/scheduler.production.js +340 -0
- package/dist/consumer-smoke/node_modules/.pnpm/scheduler@0.27.0/node_modules/scheduler/index.js +7 -0
- package/dist/consumer-smoke/node_modules/.pnpm/scheduler@0.27.0/node_modules/scheduler/index.native.js +7 -0
- package/dist/consumer-smoke/node_modules/.pnpm/scheduler@0.27.0/node_modules/scheduler/package.json +27 -0
- package/dist/consumer-smoke/node_modules/.pnpm/scheduler@0.27.0/node_modules/scheduler/unstable_mock.js +7 -0
- package/dist/consumer-smoke/node_modules/.pnpm/scheduler@0.27.0/node_modules/scheduler/unstable_post_task.js +7 -0
- package/dist/consumer-smoke/node_modules/.pnpm-workspace-state-v1.json +29 -0
- package/dist/contracts/index.d.ts +15 -0
- package/dist/contracts/index.js +3 -0
- package/dist/contracts/index.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +368 -0
- package/dist/index.js.map +1 -0
- package/dist/media-delivery/index.d.ts +185 -0
- package/dist/media-delivery/index.js +91 -0
- package/dist/media-delivery/index.js.map +1 -0
- package/dist/media-element-C9dis4aG.d.ts +40 -0
- package/dist/player-DE9Vqztu.d.ts +212 -0
- package/dist/ports/index.d.ts +32 -0
- package/dist/ports/index.js +3 -0
- package/dist/ports/index.js.map +1 -0
- package/dist/react/index.d.ts +86 -0
- package/dist/react/index.js +2393 -0
- package/dist/react/index.js.map +1 -0
- package/dist/testing/index.d.ts +36 -0
- package/dist/testing/index.js +365 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/ui/index.d.ts +143 -0
- package/dist/ui/index.js +1344 -0
- package/dist/ui/index.js.map +1 -0
- package/dist/ui/player-tokens.css +572 -0
- package/dist/ui/player-views.css +654 -0
- package/dist/ui/styles.css +2 -0
- 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
|