@videojs/soundcloud-audio 0.0.0-stage → 10.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,731 @@
1
+ import { WIDGET_ORIGIN, createSoundCloudCommand, isSoundCloudProgress, loadedShareOf, parseSoundCloudMessage } from "./widget-api.js";
2
+ import { buildSoundCloudWidgetUrl, parseSoundCloudSource as parseSoundCloudSource$1, wantsSoundCloudAutoplay } from "./source.js";
3
+ import { MediaError, NO_TIME_RANGES_YET } from "@videojs/media";
4
+ import { MediaPlayedRangesMixin, createTimeRange } from "@videojs/media/dom";
5
+ import { tryCall } from "@videojs/utils/function";
6
+ import { clamp } from "@videojs/utils/number";
7
+ import { deepEqual } from "@videojs/utils/object";
8
+ import { isNumber, isObject, isString, isUndefined } from "@videojs/utils/predicate";
9
+ //#region src/adapter.ts
10
+ /**
11
+ * Plays SoundCloud tracks and sets through the SoundCloud Widget. A set plays as one source: moving to another track
12
+ * reports a new `duration` and restarts `currentTime`, and `ended` fires once the last track ends. The widget stays
13
+ * visible and takes input, since it is what credits the uploader and SoundCloud; what it does is reported like anything
14
+ * the host asked for.
15
+ *
16
+ * @fires sourcechange - Fired when `source` changes, either directly or by resolving a new `src`. Read `source` for the
17
+ * new value.
18
+ * @fires contentdatachange - Fired when the widget reports the title and artwork of a track, and when they are cleared.
19
+ * Read `contentData` for them.
20
+ */
21
+ var SoundCloudAdapter = class SoundCloudAdapter extends MediaPlayedRangesMixin(EventTarget) {
22
+ static {
23
+ this.defaultProps = {
24
+ src: "",
25
+ autoplay: false,
26
+ defaultMuted: false,
27
+ muted: false,
28
+ loop: false,
29
+ controls: false,
30
+ playsInline: true,
31
+ preload: "metadata",
32
+ poster: "",
33
+ source: null
34
+ };
35
+ }
36
+ static {
37
+ this.PLAYER_SOFTWARE_NAME = "soundcloud-audio";
38
+ }
39
+ #target = null;
40
+ #listeners = null;
41
+ #attachId = 0;
42
+ #loadId = 0;
43
+ #adoptedSrc = null;
44
+ #src = SoundCloudAdapter.defaultProps.src;
45
+ #autoplay = SoundCloudAdapter.defaultProps.autoplay;
46
+ #defaultMuted = SoundCloudAdapter.defaultProps.defaultMuted;
47
+ #loop = SoundCloudAdapter.defaultProps.loop;
48
+ #controls = SoundCloudAdapter.defaultProps.controls;
49
+ #playsInline = SoundCloudAdapter.defaultProps.playsInline;
50
+ #preload = SoundCloudAdapter.defaultProps.preload;
51
+ #poster = SoundCloudAdapter.defaultProps.poster;
52
+ #source = SoundCloudAdapter.defaultProps.source;
53
+ #parsed = null;
54
+ #frameLoaded = true;
55
+ #widgetId = null;
56
+ #navigatedAt = 0;
57
+ #staleWidgetId = null;
58
+ #ready = false;
59
+ #requests = /* @__PURE__ */ new Map();
60
+ #playRequested = false;
61
+ #autoplayPending = false;
62
+ #startTime = null;
63
+ #soundIds = [];
64
+ #soundId = null;
65
+ #finished = false;
66
+ #awaitingProgress = false;
67
+ #playFrom = 0;
68
+ #reportedTime = 0;
69
+ #endTimer;
70
+ #durationTimer;
71
+ #seekTimer;
72
+ #paused = true;
73
+ #ended = false;
74
+ #seeking = false;
75
+ #currentTime = 0;
76
+ #duration = NaN;
77
+ #bufferedStart = 0;
78
+ #bufferedEnd = 0;
79
+ #volume = 1;
80
+ #muted = false;
81
+ #readyState = READY_STATE_HAVE_NOTHING;
82
+ #error = null;
83
+ #contentData = {};
84
+ /** The widget's window, which commands are posted to. Null until rendered. */
85
+ get engine() {
86
+ return this.#target?.contentWindow ?? null;
87
+ }
88
+ get target() {
89
+ return this.#target;
90
+ }
91
+ /** Bind the iframe hosting the widget. The widget follows once a `src` resolves, which may be after attach. */
92
+ attach(target) {
93
+ if (!target || this.#target === target) return;
94
+ if (this.#target) this.detach();
95
+ this.#target = target;
96
+ this.#listen(target);
97
+ this.#createPlayer(target);
98
+ }
99
+ detach() {
100
+ if (!this.#target) return;
101
+ this.#attachId++;
102
+ this.#loadId++;
103
+ this.#listeners?.abort();
104
+ this.#listeners = null;
105
+ this.#target = null;
106
+ this.#adoptedSrc = null;
107
+ this.#playRequested = false;
108
+ this.#resetState();
109
+ }
110
+ destroy() {
111
+ this.detach();
112
+ super.destroy();
113
+ }
114
+ get src() {
115
+ return this.#src;
116
+ }
117
+ /** SoundCloud URL. Setting it re-derives `source`, carrying its engine options over. */
118
+ set src(value) {
119
+ const { engine } = this.#source ?? {};
120
+ const next = {
121
+ ...engine && { engine },
122
+ ...value && { src: value }
123
+ };
124
+ this.source = Object.keys(next).length > 0 ? next : null;
125
+ }
126
+ get currentSrc() {
127
+ return this.#target?.getAttribute("src") ?? "";
128
+ }
129
+ get readyState() {
130
+ return this.#readyState;
131
+ }
132
+ /** Point the widget at the current source; rewriting the iframe URL is the only load the widget takes parameters on. */
133
+ async load() {
134
+ if (!this.#target) return;
135
+ const loadId = ++this.#loadId;
136
+ await Promise.resolve();
137
+ const target = this.#target;
138
+ if (loadId !== this.#loadId || !target) return;
139
+ const adopted = this.#adoptedSrc;
140
+ this.#adoptedSrc = null;
141
+ if (adopted && adopted === this.#buildWidgetUrl() && this.#hasReferrerPolicy(target)) {
142
+ this.#parsed = parseSoundCloudSource$1(this.#src);
143
+ this.#startTime = this.#parsed?.startTime ?? null;
144
+ this.#autoplayPending = true;
145
+ return;
146
+ }
147
+ this.#resetState();
148
+ this.dispatchEvent(new Event("emptied"));
149
+ this.#loadWidget(target);
150
+ }
151
+ get paused() {
152
+ return this.#paused;
153
+ }
154
+ get ended() {
155
+ return this.#ended;
156
+ }
157
+ get seeking() {
158
+ return this.#seeking;
159
+ }
160
+ async play() {
161
+ if (!this.#src || this.#error) return;
162
+ if (!this.#ready) {
163
+ this.#playRequested = true;
164
+ return;
165
+ }
166
+ if (this.#ended && this.#soundIds.length > 1 && this.#soundId !== this.#soundIds[0]) this.#post("skip", 0);
167
+ else this.#post("play");
168
+ }
169
+ pause() {
170
+ this.#playRequested = false;
171
+ this.#autoplayPending = false;
172
+ if (this.#ready) this.#post("pause");
173
+ }
174
+ get currentTime() {
175
+ return this.#currentTime;
176
+ }
177
+ set currentTime(value) {
178
+ const time = this.#clampTime(value);
179
+ if (!this.#ready && (!this.#src || this.#error)) {
180
+ this.#currentTime = time;
181
+ return;
182
+ }
183
+ if (time === this.#currentTime && !this.#ended) return;
184
+ this.#finished = false;
185
+ this.#ended = false;
186
+ this.#seeking = true;
187
+ this.#currentTime = time;
188
+ this.#reportedTime = time;
189
+ this.dispatchEvent(new Event("seeking"));
190
+ this.dispatchEvent(new Event("timeupdate"));
191
+ if (this.#ready) this.#seek(time);
192
+ }
193
+ get duration() {
194
+ return this.#duration;
195
+ }
196
+ get volume() {
197
+ return this.#volume;
198
+ }
199
+ set volume(value) {
200
+ const volume = clamp(value, 0, 1);
201
+ if (volume === this.#volume) return;
202
+ this.#volume = volume;
203
+ this.dispatchEvent(new Event("volumechange"));
204
+ if (!this.#muted) this.#applyVolume();
205
+ }
206
+ get muted() {
207
+ return this.#muted;
208
+ }
209
+ set muted(value) {
210
+ if (this.#muted === value) return;
211
+ this.#muted = value;
212
+ this.dispatchEvent(new Event("volumechange"));
213
+ this.#applyVolume();
214
+ }
215
+ /** Starts playback once the widget is ready, after the volume and start position apply. Browsers may refuse it. */
216
+ get autoplay() {
217
+ return this.#autoplay;
218
+ }
219
+ set autoplay(value) {
220
+ this.#autoplay = value;
221
+ }
222
+ get defaultMuted() {
223
+ return this.#defaultMuted;
224
+ }
225
+ set defaultMuted(value) {
226
+ this.#defaultMuted = value;
227
+ if (!this.#ready) this.#muted = value;
228
+ }
229
+ /** Replays the track, or a set from its first track, when the last one ends. */
230
+ get loop() {
231
+ return this.#loop;
232
+ }
233
+ set loop(value) {
234
+ this.#loop = value;
235
+ }
236
+ get controls() {
237
+ return this.#controls;
238
+ }
239
+ set controls(value) {
240
+ this.#controls = value;
241
+ }
242
+ get playsInline() {
243
+ return this.#playsInline;
244
+ }
245
+ set playsInline(value) {
246
+ this.#playsInline = value;
247
+ }
248
+ get preload() {
249
+ return this.#preload;
250
+ }
251
+ set preload(value) {
252
+ this.#preload = value;
253
+ }
254
+ get poster() {
255
+ return this.#poster;
256
+ }
257
+ set poster(value) {
258
+ this.#poster = value;
259
+ }
260
+ /** SoundCloud URL in `src`, widget parameters under `engine.soundcloud`. Replacing it re-derives `src`. */
261
+ get source() {
262
+ return this.#source;
263
+ }
264
+ set source(value) {
265
+ const source = value ?? null;
266
+ if (source === this.#source) return;
267
+ const src = source?.src ?? "";
268
+ const srcChanged = this.#src !== src;
269
+ const engineChanged = !deepEqual(this.#source?.engine?.soundcloud ?? null, source?.engine?.soundcloud ?? null);
270
+ this.#source = source;
271
+ this.#src = src;
272
+ if (srcChanged || engineChanged) this.load();
273
+ this.dispatchEvent(new Event("sourcechange"));
274
+ }
275
+ /** The stretch the widget reports loaded, from where playback last started or landed. */
276
+ get buffered() {
277
+ return this.#bufferedEnd > this.#bufferedStart ? createTimeRange(this.#bufferedStart, this.#bufferedEnd) : NO_TIME_RANGES_YET;
278
+ }
279
+ get seekable() {
280
+ return this.#duration > 0 && Number.isFinite(this.#duration) ? createTimeRange(0, this.#duration) : NO_TIME_RANGES_YET;
281
+ }
282
+ get error() {
283
+ return this.#error;
284
+ }
285
+ /**
286
+ * The `title` and `poster` of the track playing, which change with each track of a set. Empty until the widget
287
+ * reports them, and again across a source change. `contentdatachange` announces both.
288
+ */
289
+ get contentData() {
290
+ return this.#contentData;
291
+ }
292
+ /** Unsupported: SoundCloud has no captions or other text tracks. */
293
+ get textTracks() {}
294
+ #createPlayer(target) {
295
+ const rendered = target.getAttribute("src");
296
+ if (!rendered) {
297
+ if (this.#src) this.#loadWidget(target);
298
+ return;
299
+ }
300
+ this.#frameLoaded = hasWidgetDocument(target);
301
+ this.#navigatedAt = 0;
302
+ this.#staleWidgetId = null;
303
+ this.dispatchEvent(new Event("loadstart"));
304
+ this.#probeReady();
305
+ this.#adoptedSrc = rendered;
306
+ if (this.#src) this.load();
307
+ }
308
+ #loadWidget(target) {
309
+ const parsed = parseSoundCloudSource$1(this.#src);
310
+ this.#parsed = parsed;
311
+ this.#startTime = parsed?.startTime ?? null;
312
+ if (!this.#src) {
313
+ target.removeAttribute("src");
314
+ return;
315
+ }
316
+ this.#autoplayPending = true;
317
+ this.dispatchEvent(new Event("loadstart"));
318
+ if (!parsed) {
319
+ target.removeAttribute("src");
320
+ this.#fail(new MediaError(`Unrecognized SoundCloud source: ${this.#src}`, MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED));
321
+ return;
322
+ }
323
+ this.#navigate(target, buildSoundCloudWidgetUrl(parsed.url, parsed, this.#snapshotProps()));
324
+ }
325
+ #navigate(target, url) {
326
+ const referrerPolicy = this.#source?.engine?.soundcloud?.referrerPolicy;
327
+ if (referrerPolicy !== void 0) target.referrerPolicy = referrerPolicy;
328
+ this.#frameLoaded = false;
329
+ this.#navigatedAt = Date.now();
330
+ target.src = url;
331
+ }
332
+ #hasReferrerPolicy(target) {
333
+ const referrerPolicy = this.#source?.engine?.soundcloud?.referrerPolicy;
334
+ return referrerPolicy === void 0 || target.referrerPolicy === referrerPolicy;
335
+ }
336
+ #buildWidgetUrl() {
337
+ const parsed = parseSoundCloudSource$1(this.#src);
338
+ if (!parsed) return "";
339
+ return buildSoundCloudWidgetUrl(parsed.url, parsed, this.#snapshotProps());
340
+ }
341
+ #listen(target) {
342
+ const win = globalThis.window;
343
+ if (isUndefined(win)) return;
344
+ const attachId = this.#attachId;
345
+ this.#listeners = new AbortController();
346
+ const { signal } = this.#listeners;
347
+ win.addEventListener("message", (event) => this.#onMessage(event, target, attachId), { signal });
348
+ target.addEventListener("load", () => {
349
+ this.#frameLoaded = hasWidgetDocument(target);
350
+ if (!this.#ready) this.#probeReady();
351
+ }, { signal });
352
+ }
353
+ #onMessage(event, target, attachId) {
354
+ if (attachId !== this.#attachId || event.origin !== "https://w.soundcloud.com") return;
355
+ const frame = target.contentWindow;
356
+ if (!frame || event.source !== frame) return;
357
+ const message = parseSoundCloudMessage(event.data);
358
+ if (!message) return;
359
+ const { method, value, widgetId = null } = message;
360
+ if (!this.#ready) {
361
+ if ((method === "ready" || method === READY_PROBE && this.#frameLoaded) && this.#isCurrentDocument(widgetId)) this.#onReady(widgetId);
362
+ return;
363
+ }
364
+ if (method === "ready" && widgetId !== this.#widgetId && this.#isCurrentDocument(widgetId)) {
365
+ this.#onFrameReloaded(widgetId);
366
+ return;
367
+ }
368
+ if (widgetId !== this.#widgetId) return;
369
+ const answer = this.#requests.get(method)?.shift();
370
+ if (answer) {
371
+ answer(value);
372
+ return;
373
+ }
374
+ switch (method) {
375
+ case "play":
376
+ if (isSoundCloudProgress(value)) this.#onWidgetPlay(value);
377
+ break;
378
+ case "pause":
379
+ if (isSoundCloudProgress(value)) this.#onWidgetPause(value);
380
+ break;
381
+ case "finish":
382
+ if (isSoundCloudProgress(value)) this.#onWidgetFinish(value);
383
+ break;
384
+ case "seek":
385
+ if (isSoundCloudProgress(value)) this.#onWidgetSeek(value);
386
+ break;
387
+ case "playProgress":
388
+ if (isSoundCloudProgress(value)) this.#onWidgetProgress(value);
389
+ break;
390
+ case "loadProgress":
391
+ if (isSoundCloudProgress(value)) this.#syncBuffered(value);
392
+ break;
393
+ case "error": this.#onWidgetError();
394
+ }
395
+ }
396
+ #onReady(widgetId) {
397
+ this.#ready = true;
398
+ this.#widgetId = widgetId;
399
+ for (const type of SUBSCRIBED_EVENTS) this.#post("addEventListener", type);
400
+ this.#get("getSounds").then((sounds) => {
401
+ this.#soundIds = Array.isArray(sounds) ? sounds.map(soundIdOf).filter(isNumber) : [];
402
+ });
403
+ this.#get("getCurrentSound").then((sound) => {
404
+ this.#soundId = soundIdOf(sound) ?? null;
405
+ this.#setContentData(sound);
406
+ });
407
+ this.#get("getDuration").then((duration) => this.#onLoaded(duration));
408
+ }
409
+ #onFrameReloaded(widgetId) {
410
+ this.#resetState();
411
+ this.#startTime = this.#parsed?.startTime ?? null;
412
+ this.dispatchEvent(new Event("emptied"));
413
+ this.dispatchEvent(new Event("loadstart"));
414
+ this.#onReady(widgetId);
415
+ }
416
+ #onLoaded(duration, attempt = 1) {
417
+ if (this.#error) return;
418
+ if (!isNumber(duration) || !(duration > 0)) {
419
+ if (attempt >= DURATION_ATTEMPTS) {
420
+ this.#fail(new MediaError("The SoundCloud widget did not finish loading in time.", MediaError.MEDIA_ERR_NETWORK));
421
+ return;
422
+ }
423
+ const widgetId = this.#widgetId;
424
+ this.#durationTimer = setTimeout(() => {
425
+ if (this.#ready && this.#widgetId === widgetId) this.#get("getDuration").then((next) => this.#onLoaded(next, attempt + 1));
426
+ }, DURATION_RETRY_INTERVAL);
427
+ return;
428
+ }
429
+ this.#applyDuration(duration / 1e3);
430
+ this.#readyState = READY_STATE_HAVE_ENOUGH_DATA;
431
+ for (const type of [
432
+ "loadedmetadata",
433
+ "loadeddata",
434
+ "canplay",
435
+ "canplaythrough",
436
+ "loadcomplete"
437
+ ]) this.dispatchEvent(new Event(type));
438
+ this.#syncToWidget();
439
+ this.#get("isPaused").then((paused) => {
440
+ if (paused === false && this.#paused) this.#onWidgetPlay({ currentPosition: this.#currentTime * 1e3 });
441
+ });
442
+ }
443
+ #syncToWidget() {
444
+ this.#applyVolume();
445
+ if (this.#seeking) {
446
+ this.#currentTime = this.#clampTime(this.#currentTime);
447
+ this.#seek(this.#currentTime);
448
+ } else if (isNumber(this.#startTime) && this.#startTime > 0) this.currentTime = this.#startTime;
449
+ const autoplay = this.#autoplayPending && (this.#autoplay || wantsSoundCloudAutoplay(this.#parsed, this.#source));
450
+ if (this.#playRequested || autoplay) this.#post("play");
451
+ this.#playRequested = false;
452
+ this.#autoplayPending = false;
453
+ }
454
+ #onWidgetPlay(progress) {
455
+ this.#syncSound(progress);
456
+ this.#finished = false;
457
+ clearTimeout(this.#endTimer);
458
+ if (!this.#paused) return;
459
+ const position = (progress.currentPosition ?? 0) / 1e3;
460
+ const loaded = (loadedShareOf(progress) ?? 0) * this.#duration;
461
+ this.#paused = false;
462
+ this.#ended = false;
463
+ this.dispatchEvent(new Event("play"));
464
+ if (loaded > position + STARVED_MARGIN) {
465
+ this.dispatchEvent(new Event("playing"));
466
+ return;
467
+ }
468
+ this.#awaitingProgress = true;
469
+ this.#playFrom = position;
470
+ this.#readyState = READY_STATE_HAVE_CURRENT_DATA;
471
+ this.dispatchEvent(new Event("waiting"));
472
+ }
473
+ #onWidgetPause(progress) {
474
+ this.#syncSound(progress);
475
+ if (this.#isAtEnd(progress)) {
476
+ clearTimeout(this.#endTimer);
477
+ this.#endTimer = setTimeout(() => this.#onPause(), END_GRACE);
478
+ return;
479
+ }
480
+ this.#onPause();
481
+ }
482
+ #onWidgetFinish(progress) {
483
+ clearTimeout(this.#endTimer);
484
+ this.#syncSound(progress);
485
+ const index = this.#soundId === null ? -1 : this.#soundIds.indexOf(this.#soundId);
486
+ if (!(this.#soundIds.length === 0 || index === this.#soundIds.length - 1)) {
487
+ this.#endTimer = setTimeout(() => this.#end(), END_GRACE);
488
+ return;
489
+ }
490
+ if (!this.#loop) {
491
+ this.#end();
492
+ return;
493
+ }
494
+ this.#applyCurrentTime(0, true);
495
+ if (this.#soundIds.length > 1) this.#post("skip", 0);
496
+ else this.#post("play");
497
+ this.#endTimer = setTimeout(() => this.#end(), END_GRACE);
498
+ }
499
+ #onWidgetSeek(progress) {
500
+ this.#syncSound(progress);
501
+ if (this.#finished) return;
502
+ const time = this.#clampTime((progress.currentPosition ?? 0) / 1e3);
503
+ this.#setBuffered(time, this.#loadedEnd(progress) ?? this.#bufferedEnd);
504
+ if (this.#seeking) {
505
+ this.#settleSeek(time);
506
+ return;
507
+ }
508
+ if (Math.abs(time - this.#currentTime) < SEEK_TOLERANCE) return;
509
+ this.#ended = false;
510
+ this.#seeking = true;
511
+ this.dispatchEvent(new Event("seeking"));
512
+ this.#settleSeek(time);
513
+ }
514
+ #onWidgetProgress(progress) {
515
+ this.#syncSound(progress);
516
+ if (this.#finished) return;
517
+ this.#syncBuffered(progress);
518
+ if (this.#seeking) return;
519
+ const time = (progress.currentPosition ?? 0) / 1e3;
520
+ this.#applyCurrentTime(time);
521
+ if (this.#awaitingProgress && !this.#paused && time > this.#playFrom) {
522
+ this.#awaitingProgress = false;
523
+ this.#readyState = READY_STATE_HAVE_ENOUGH_DATA;
524
+ this.dispatchEvent(new Event("playing"));
525
+ }
526
+ }
527
+ #onWidgetError() {
528
+ if (this.#readyState < READY_STATE_HAVE_METADATA) {
529
+ this.#fail(new MediaError(`SoundCloud could not play ${this.#src}. It may be private, removed, or not available in this region.`, MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED));
530
+ this.#onPause();
531
+ return;
532
+ }
533
+ const error = new MediaError("The SoundCloud widget reported an error.", MediaError.MEDIA_ERR_CUSTOM, false);
534
+ this.dispatchEvent(new ErrorEvent("error", {
535
+ error,
536
+ message: error.message
537
+ }));
538
+ this.#get("isPaused").then((paused) => {
539
+ if (paused === true) this.#onPause();
540
+ });
541
+ }
542
+ #syncSound({ soundId }) {
543
+ if (!isNumber(soundId) || soundId === this.#soundId) return;
544
+ const changed = this.#soundId !== null;
545
+ this.#soundId = soundId;
546
+ if (!changed) return;
547
+ clearTimeout(this.#endTimer);
548
+ this.#finished = false;
549
+ this.#ended = false;
550
+ this.#setBuffered(0, 0);
551
+ this.#applyCurrentTime(0, true);
552
+ this.#get("getDuration").then((duration) => {
553
+ if (isNumber(duration) && duration > 0) this.#applyDuration(duration / 1e3);
554
+ });
555
+ this.#get("getCurrentSound").then((sound) => {
556
+ if (soundIdOf(sound) === this.#soundId) this.#setContentData(sound);
557
+ });
558
+ }
559
+ #end() {
560
+ clearTimeout(this.#endTimer);
561
+ this.#finished = true;
562
+ this.#applyCurrentTime(this.#duration, true);
563
+ this.#onPause();
564
+ this.#ended = true;
565
+ this.dispatchEvent(new Event("ended"));
566
+ }
567
+ #onPause() {
568
+ clearTimeout(this.#endTimer);
569
+ this.#awaitingProgress = false;
570
+ if (this.#paused) return;
571
+ this.#paused = true;
572
+ this.dispatchEvent(new Event("pause"));
573
+ }
574
+ #seek(time) {
575
+ this.#post("seekTo", Math.round(time * 1e3));
576
+ clearTimeout(this.#seekTimer);
577
+ this.#seekTimer = setTimeout(() => {
578
+ this.#get("getPosition").then((position) => {
579
+ if (this.#seeking && isNumber(position)) this.#settleSeek(this.#clampTime(position / 1e3));
580
+ });
581
+ }, SEEK_SETTLE_TIMEOUT);
582
+ }
583
+ #settleSeek(time) {
584
+ clearTimeout(this.#seekTimer);
585
+ this.#applyCurrentTime(time, true);
586
+ this.#seeking = false;
587
+ this.dispatchEvent(new Event("seeked"));
588
+ }
589
+ #applyVolume() {
590
+ if (!this.#ready) return;
591
+ this.#post("setVolume", this.#muted ? 0 : Math.round(this.#volume * 100));
592
+ }
593
+ #applyDuration(duration) {
594
+ if (duration === this.#duration) return;
595
+ this.#duration = duration;
596
+ this.dispatchEvent(new Event("durationchange"));
597
+ }
598
+ #applyCurrentTime(time, force = false) {
599
+ const clamped = this.#clampTime(time);
600
+ if (clamped === this.#currentTime) return;
601
+ this.#currentTime = clamped;
602
+ if (!force && Math.abs(clamped - this.#reportedTime) < TIMEUPDATE_INTERVAL) return;
603
+ this.#reportedTime = clamped;
604
+ this.dispatchEvent(new Event("timeupdate"));
605
+ }
606
+ #syncBuffered(progress) {
607
+ const end = this.#loadedEnd(progress);
608
+ if (!isUndefined(end)) this.#setBuffered(this.#bufferedStart, end);
609
+ }
610
+ #loadedEnd(progress) {
611
+ const share = loadedShareOf(progress);
612
+ return isUndefined(share) || !Number.isFinite(this.#duration) ? void 0 : clamp(share, 0, 1) * this.#duration;
613
+ }
614
+ #setBuffered(start, end) {
615
+ if (start === this.#bufferedStart && end === this.#bufferedEnd) return;
616
+ this.#bufferedStart = start;
617
+ this.#bufferedEnd = end;
618
+ this.dispatchEvent(new Event("progress"));
619
+ }
620
+ #clampTime(time) {
621
+ const max = Number.isFinite(this.#duration) ? this.#duration : Number.POSITIVE_INFINITY;
622
+ return clamp(Number.isFinite(time) ? time : 0, 0, max);
623
+ }
624
+ #isAtEnd({ currentPosition }) {
625
+ return Number.isFinite(this.#duration) && this.#duration - (currentPosition ?? 0) / 1e3 <= END_TOLERANCE;
626
+ }
627
+ #setContentData(sound) {
628
+ const contentData = contentDataOf(sound);
629
+ if (deepEqual(contentData, this.#contentData)) return;
630
+ this.#contentData = contentData;
631
+ this.dispatchEvent(new Event("contentdatachange"));
632
+ }
633
+ #fail(error) {
634
+ this.#error = error;
635
+ this.dispatchEvent(new Event("error"));
636
+ }
637
+ #get(method) {
638
+ if (!this.#ready) return Promise.resolve(void 0);
639
+ return new Promise((resolve) => {
640
+ const queue = this.#requests.get(method) ?? [];
641
+ queue.push(resolve);
642
+ this.#requests.set(method, queue);
643
+ this.#post(method);
644
+ });
645
+ }
646
+ #isCurrentDocument(widgetId) {
647
+ if (widgetId === null) return true;
648
+ const createdAt = Number(WIDGET_CREATED_AT.exec(widgetId)?.[1]);
649
+ return Number.isFinite(createdAt) ? createdAt >= this.#navigatedAt : widgetId !== this.#staleWidgetId;
650
+ }
651
+ #probeReady() {
652
+ if (this.#frameLoaded) this.#post(READY_PROBE);
653
+ }
654
+ #post(method, value) {
655
+ const frame = this.#target?.contentWindow;
656
+ if (!frame) return;
657
+ tryCall(() => frame.postMessage(createSoundCloudCommand(method, value), WIDGET_ORIGIN));
658
+ }
659
+ #snapshotProps() {
660
+ return { source: this.#source };
661
+ }
662
+ #resetState() {
663
+ clearTimeout(this.#endTimer);
664
+ clearTimeout(this.#seekTimer);
665
+ clearTimeout(this.#durationTimer);
666
+ this.#staleWidgetId = this.#widgetId ?? this.#staleWidgetId;
667
+ this.#widgetId = null;
668
+ this.#ready = false;
669
+ this.#requests.clear();
670
+ this.#autoplayPending = false;
671
+ this.#startTime = null;
672
+ this.#soundIds = [];
673
+ this.#soundId = null;
674
+ this.#finished = false;
675
+ this.#awaitingProgress = false;
676
+ this.#playFrom = 0;
677
+ this.#reportedTime = 0;
678
+ this.#paused = true;
679
+ this.#ended = false;
680
+ this.#seeking = false;
681
+ this.#currentTime = 0;
682
+ this.#duration = NaN;
683
+ this.#bufferedStart = 0;
684
+ this.#bufferedEnd = 0;
685
+ this.#readyState = READY_STATE_HAVE_NOTHING;
686
+ this.#error = null;
687
+ this.#setContentData(null);
688
+ }
689
+ };
690
+ function hasWidgetDocument(target) {
691
+ return !!target.getAttribute("src") && target.contentDocument === null;
692
+ }
693
+ function soundIdOf(sound) {
694
+ const id = isObject(sound) ? sound.id : void 0;
695
+ return isNumber(id) ? id : isString(id) && /^\d+$/.test(id) ? Number(id) : void 0;
696
+ }
697
+ function contentDataOf(sound) {
698
+ if (!isObject(sound)) return {};
699
+ const { title, artwork_url: artwork, user } = sound;
700
+ const image = isString(artwork) && artwork ? artwork : user?.avatar_url;
701
+ return {
702
+ ...isString(title) && title && { title },
703
+ ...isString(image) && image && { poster: image.replace(ARTWORK_SIZE, "-t500x500.") }
704
+ };
705
+ }
706
+ const ARTWORK_SIZE = /-large\.(?=[a-z]+$)/;
707
+ const SUBSCRIBED_EVENTS = [
708
+ "play",
709
+ "pause",
710
+ "finish",
711
+ "seek",
712
+ "playProgress",
713
+ "loadProgress",
714
+ "error"
715
+ ];
716
+ const WIDGET_CREATED_AT = /^widget_(\d+)$/;
717
+ const READY_PROBE = "getCurrentSoundIndex";
718
+ const END_TOLERANCE = .5;
719
+ const END_GRACE = 1500;
720
+ const SEEK_TOLERANCE = .05;
721
+ const STARVED_MARGIN = .5;
722
+ const SEEK_SETTLE_TIMEOUT = 2e3;
723
+ const DURATION_ATTEMPTS = 20;
724
+ const DURATION_RETRY_INTERVAL = 250;
725
+ const TIMEUPDATE_INTERVAL = .25;
726
+ const READY_STATE_HAVE_NOTHING = 0;
727
+ const READY_STATE_HAVE_METADATA = 1;
728
+ const READY_STATE_HAVE_CURRENT_DATA = 2;
729
+ const READY_STATE_HAVE_ENOUGH_DATA = 4;
730
+ //#endregion
731
+ export { SoundCloudAdapter };