@remino/jukette-soundcloud 0.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,349 @@
1
+ /*! @remino/jukette-soundcloud v0.4.0 | (c) 2026 Rémino Rem <https://remino.net/> | ISC Licence */
2
+ let _remino_jukette_core = require("@remino/jukette-core");
3
+ //#region src/lib/soundcloud.ts
4
+ var soundCloudWidgetApiUrl = "https://w.soundcloud.com/player/api.js";
5
+ var soundCloudOEmbedUrl = "https://soundcloud.com/oembed";
6
+ var soundCloudRootAttribute = "data-jukette-soundcloud-root";
7
+ var soundCloudPlayerType = "soundcloud";
8
+ var soundCloudWidgetPromise = null;
9
+ var elementStates = /* @__PURE__ */ new WeakMap();
10
+ var hostStates = /* @__PURE__ */ new WeakMap();
11
+ var getSoundCloudWindow = () => typeof window !== "undefined" && window.SC?.Widget ? window.SC : null;
12
+ var ensureSoundCloudWidgetApi = async () => {
13
+ const available = getSoundCloudWindow();
14
+ if (available) return available;
15
+ if (soundCloudWidgetPromise) return soundCloudWidgetPromise;
16
+ if (typeof document === "undefined") throw new Error("SoundCloud widget API requires a document context.");
17
+ soundCloudWidgetPromise = new Promise((resolve, reject) => {
18
+ const existingScript = document.querySelector(`script[src="${soundCloudWidgetApiUrl}"]`);
19
+ const script = existingScript ?? document.createElement("script");
20
+ const cleanup = () => {
21
+ script.removeEventListener("load", onLoad);
22
+ script.removeEventListener("error", onError);
23
+ };
24
+ const onError = () => {
25
+ cleanup();
26
+ soundCloudWidgetPromise = null;
27
+ reject(/* @__PURE__ */ new Error("Failed to load the SoundCloud widget API."));
28
+ };
29
+ const onLoad = () => {
30
+ cleanup();
31
+ const nextAvailable = getSoundCloudWindow();
32
+ if (!nextAvailable) {
33
+ soundCloudWidgetPromise = null;
34
+ reject(/* @__PURE__ */ new Error("SoundCloud widget API did not initialize."));
35
+ return;
36
+ }
37
+ resolve(nextAvailable);
38
+ };
39
+ script.addEventListener("load", onLoad, { once: true });
40
+ script.addEventListener("error", onError, { once: true });
41
+ if (!existingScript) {
42
+ script.async = true;
43
+ script.src = soundCloudWidgetApiUrl;
44
+ document.head.append(script);
45
+ }
46
+ });
47
+ return soundCloudWidgetPromise;
48
+ };
49
+ var getHostStateKey = (track) => `${track.type ?? soundCloudPlayerType}:${track.src}`;
50
+ var parseSoundCloudMetadata = (oembed) => {
51
+ const title = oembed.title?.replace(/\s+by\s+[^]+$/, "").trim();
52
+ const artist = oembed.author_name?.trim();
53
+ if (!title && !artist) return;
54
+ return {
55
+ artist,
56
+ title
57
+ };
58
+ };
59
+ var parseSoundCloudIframeSrc = (oembed) => {
60
+ if (!oembed.html || typeof document === "undefined") return null;
61
+ const template = document.createElement("template");
62
+ template.innerHTML = oembed.html;
63
+ return template.content.querySelector("iframe")?.getAttribute("src") ?? null;
64
+ };
65
+ var createHiddenIframe = (src) => {
66
+ const iframe = document.createElement("iframe");
67
+ iframe.src = src;
68
+ iframe.hidden = true;
69
+ iframe.tabIndex = -1;
70
+ iframe.title = "SoundCloud player";
71
+ iframe.setAttribute("aria-hidden", "true");
72
+ iframe.setAttribute("allow", "autoplay; encrypted-media");
73
+ iframe.setAttribute("data-jukette-soundcloud-iframe", "");
74
+ iframe.style.blockSize = "0";
75
+ iframe.style.border = "0";
76
+ iframe.style.inlineSize = "0";
77
+ iframe.style.opacity = "0";
78
+ iframe.style.pointerEvents = "none";
79
+ iframe.style.position = "absolute";
80
+ return iframe;
81
+ };
82
+ var getSoundCloudRoot = (host) => {
83
+ const rootNode = host.shadowRoot ?? host;
84
+ const existing = rootNode.querySelector(`[${soundCloudRootAttribute}]`);
85
+ if (existing) return existing;
86
+ const root = document.createElement("div");
87
+ root.hidden = true;
88
+ root.setAttribute(soundCloudRootAttribute, "");
89
+ root.style.display = "none";
90
+ rootNode.append(root);
91
+ return root;
92
+ };
93
+ var SoundCloudTrackState = class {
94
+ track;
95
+ host;
96
+ trackElement;
97
+ activeTrack = null;
98
+ durationSeconds = 0;
99
+ iframeElement = null;
100
+ metadata;
101
+ oEmbedPromise = null;
102
+ playRequested = false;
103
+ playing = false;
104
+ positionSeconds = 0;
105
+ ready = false;
106
+ widget = null;
107
+ widgetReadyPromise = null;
108
+ widgetReadyReject = null;
109
+ widgetReadyResolve = null;
110
+ constructor(track, host, trackElement) {
111
+ this.track = track;
112
+ this.host = host;
113
+ this.trackElement = trackElement;
114
+ }
115
+ get currentTime() {
116
+ return this.positionSeconds;
117
+ }
118
+ get duration() {
119
+ return this.durationSeconds;
120
+ }
121
+ attach(track) {
122
+ this.activeTrack = track;
123
+ }
124
+ detach(track) {
125
+ if (this.activeTrack === track) {
126
+ this.activeTrack = null;
127
+ this.playRequested = false;
128
+ this.playing = false;
129
+ }
130
+ }
131
+ async preload(options) {
132
+ if (!options.prepare && !options.preloadMetadata) return;
133
+ if (options.preloadMetadata || options.prepare) await this.ensureOEmbed();
134
+ if (options.prepare) await this.ensurePrepared();
135
+ return {
136
+ duration: this.durationSeconds || void 0,
137
+ metadata: this.metadata
138
+ };
139
+ }
140
+ async load(_options) {
141
+ this.playRequested = false;
142
+ this.playing = false;
143
+ this.activeTrack?.trackCallbacks.onStatus("Loading SoundCloud");
144
+ await this.ensureOEmbed();
145
+ await this.ensurePrepared();
146
+ this.widget?.pause();
147
+ this.seekTo(0);
148
+ this.activeTrack?.trackCallbacks.onMetadata(this.metadata ?? {});
149
+ if (this.durationSeconds > 0) this.activeTrack?.trackCallbacks.onDuration(this.durationSeconds);
150
+ this.activeTrack?.trackCallbacks.onProgress(this.positionSeconds, this.durationSeconds);
151
+ this.activeTrack?.trackCallbacks.onReady();
152
+ this.activeTrack?.trackCallbacks.onStatus();
153
+ }
154
+ async play(options) {
155
+ await this.ensurePrepared();
156
+ if (options.isStale()) return false;
157
+ if (!this.widget) return false;
158
+ if (options.restart || this.durationSeconds > 0 && this.positionSeconds >= this.durationSeconds) this.seekTo(0);
159
+ this.playRequested = true;
160
+ this.activeTrack?.trackCallbacks.onStatus("Starting SoundCloud");
161
+ this.widget.play();
162
+ return false;
163
+ }
164
+ pause() {
165
+ this.playRequested = false;
166
+ this.playing = false;
167
+ this.widget?.pause();
168
+ }
169
+ seekTo(seconds) {
170
+ const safeSeconds = Math.max(0, seconds);
171
+ this.positionSeconds = safeSeconds;
172
+ this.widget?.seekTo(Math.round(safeSeconds * 1e3));
173
+ }
174
+ async requestPosition() {
175
+ if (!this.widget) return;
176
+ const [position, duration] = await Promise.all([new Promise((resolve) => this.widget?.getPosition((value) => resolve(value / 1e3))), new Promise((resolve) => this.widget?.getDuration((value) => resolve(value / 1e3)))]);
177
+ if (duration > 0) this.durationSeconds = duration;
178
+ if (position >= 0) this.positionSeconds = position;
179
+ this.activeTrack?.trackCallbacks.onProgress(this.positionSeconds, this.durationSeconds);
180
+ }
181
+ async ensureOEmbed() {
182
+ if (this.oEmbedPromise) return this.oEmbedPromise;
183
+ if (typeof fetch === "undefined") throw new Error("SoundCloud oEmbed requires fetch support.");
184
+ const params = new URLSearchParams({
185
+ auto_play: "false",
186
+ buying: "false",
187
+ download: "false",
188
+ format: "json",
189
+ maxheight: "166",
190
+ sharing: "false",
191
+ show_artwork: "false",
192
+ show_comments: "false",
193
+ show_playcount: "false",
194
+ show_user: "true",
195
+ url: this.track.src
196
+ });
197
+ this.oEmbedPromise = fetch(`${soundCloudOEmbedUrl}?${params.toString()}`).then(async (response) => {
198
+ if (!response.ok) throw new Error("SoundCloud oEmbed request failed.");
199
+ return await response.json();
200
+ }).then((oembed) => {
201
+ this.metadata = parseSoundCloudMetadata(oembed);
202
+ return oembed;
203
+ });
204
+ return this.oEmbedPromise;
205
+ }
206
+ async ensurePrepared() {
207
+ if (this.ready) return;
208
+ const api = await ensureSoundCloudWidgetApi();
209
+ const iframeSrc = parseSoundCloudIframeSrc(await this.ensureOEmbed());
210
+ if (!iframeSrc) throw new Error("SoundCloud oEmbed did not include an iframe.");
211
+ if (!this.iframeElement) {
212
+ this.iframeElement = createHiddenIframe(iframeSrc);
213
+ getSoundCloudRoot(this.host).append(this.iframeElement);
214
+ }
215
+ if (!this.widget) {
216
+ this.widget = api.Widget(this.iframeElement);
217
+ this.widgetReadyPromise = new Promise((resolve, reject) => {
218
+ this.widgetReadyResolve = resolve;
219
+ this.widgetReadyReject = reject;
220
+ });
221
+ this.bindWidgetEvents(api.Widget.Events);
222
+ }
223
+ await this.widgetReadyPromise;
224
+ await this.readDuration();
225
+ }
226
+ bindWidgetEvents(events) {
227
+ if (!this.widget) return;
228
+ this.widget.bind(events.READY, () => {
229
+ this.ready = true;
230
+ this.widgetReadyResolve?.();
231
+ });
232
+ this.widget.bind(events.PLAY_PROGRESS, (payload) => {
233
+ if (!this.playing) return;
234
+ const position = (payload?.currentPosition ?? 0) / 1e3;
235
+ if (Number.isFinite(position)) this.positionSeconds = position;
236
+ this.activeTrack?.trackCallbacks.onProgress(this.positionSeconds, this.durationSeconds);
237
+ });
238
+ this.widget.bind(events.PLAY, () => {
239
+ if (!this.playRequested && !this.playing) return;
240
+ this.playing = true;
241
+ this.activeTrack?.handlePlayEvent();
242
+ });
243
+ this.widget.bind(events.PAUSE, () => {
244
+ this.playRequested = false;
245
+ this.playing = false;
246
+ this.activeTrack?.handlePauseEvent();
247
+ });
248
+ this.widget.bind(events.FINISH, () => {
249
+ this.playRequested = false;
250
+ this.playing = false;
251
+ this.positionSeconds = this.durationSeconds;
252
+ this.activeTrack?.trackCallbacks.onFinish();
253
+ });
254
+ this.widget.bind(events.ERROR, () => {
255
+ this.widgetReadyReject?.(/* @__PURE__ */ new Error("SoundCloud widget reported an error."));
256
+ this.activeTrack?.trackCallbacks.onStatus("SoundCloud playback failed");
257
+ });
258
+ }
259
+ async readDuration() {
260
+ if (!this.widget) return;
261
+ const duration = await new Promise((resolve) => this.widget?.getDuration((value) => resolve(value / 1e3)));
262
+ if (duration > 0) {
263
+ this.durationSeconds = duration;
264
+ this.activeTrack?.trackCallbacks.onDuration(this.durationSeconds);
265
+ }
266
+ }
267
+ };
268
+ var SoundCloudPlayableTrack = class extends _remino_jukette_core.JukettePlayableTrack {
269
+ state;
270
+ ignoreNextPauseEvent = false;
271
+ constructor(track, callbacks, state) {
272
+ super(track, callbacks);
273
+ this.state = state;
274
+ this.state.attach(this);
275
+ }
276
+ get trackCallbacks() {
277
+ return this.callbacks;
278
+ }
279
+ get currentTime() {
280
+ return this.state.currentTime;
281
+ }
282
+ get duration() {
283
+ return this.state.duration;
284
+ }
285
+ load(options) {
286
+ return this.state.load(options);
287
+ }
288
+ play(options) {
289
+ return this.state.play(options);
290
+ }
291
+ pause(_options = {}) {
292
+ this.ignoreNextPauseEvent = true;
293
+ this.state.pause();
294
+ }
295
+ seek(seconds) {
296
+ this.state.seekTo(seconds);
297
+ this.callbacks.onProgress(seconds, this.duration);
298
+ }
299
+ stop() {
300
+ this.pause({ silent: true });
301
+ this.state.detach(this);
302
+ }
303
+ requestPosition() {
304
+ this.state.requestPosition();
305
+ }
306
+ handlePauseEvent() {
307
+ if (this.ignoreNextPauseEvent) {
308
+ this.ignoreNextPauseEvent = false;
309
+ return;
310
+ }
311
+ this.callbacks.onPause();
312
+ }
313
+ handlePlayEvent() {
314
+ this.callbacks.onStatus();
315
+ this.callbacks.onPlay();
316
+ }
317
+ };
318
+ var createSoundCloudTrackState = (track, host, trackElement) => {
319
+ if (trackElement) {
320
+ const current = elementStates.get(trackElement);
321
+ if (current) return current;
322
+ const next = new SoundCloudTrackState(track, host, trackElement);
323
+ elementStates.set(trackElement, next);
324
+ return next;
325
+ }
326
+ let states = hostStates.get(host);
327
+ if (!states) {
328
+ states = /* @__PURE__ */ new Map();
329
+ hostStates.set(host, states);
330
+ }
331
+ const key = getHostStateKey(track);
332
+ const current = states.get(key);
333
+ if (current) return current;
334
+ const next = new SoundCloudTrackState(track, host, null);
335
+ states.set(key, next);
336
+ return next;
337
+ };
338
+ var soundCloudBackend = {
339
+ createPlayableTrack(track, callbacks, options) {
340
+ return new SoundCloudPlayableTrack(track, callbacks, createSoundCloudTrackState(track, options.host, options.trackElement));
341
+ },
342
+ preloadTrack: async (track, options) => createSoundCloudTrackState(track, options.host, options.trackElement).preload(options),
343
+ type: soundCloudPlayerType
344
+ };
345
+ var register = () => (0, _remino_jukette_core.registerJuketteBackend)(soundCloudBackend);
346
+ //#endregion
347
+ //#region src/lib/soundcloud-auto.ts
348
+ register();
349
+ //#endregion
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,349 @@
1
+ /*! @remino/jukette-soundcloud v0.4.0 | (c) 2026 Rémino Rem <https://remino.net/> | ISC Licence */
2
+ import { JukettePlayableTrack, registerJuketteBackend } from "@remino/jukette-core";
3
+ //#region src/lib/soundcloud.ts
4
+ var soundCloudWidgetApiUrl = "https://w.soundcloud.com/player/api.js";
5
+ var soundCloudOEmbedUrl = "https://soundcloud.com/oembed";
6
+ var soundCloudRootAttribute = "data-jukette-soundcloud-root";
7
+ var soundCloudPlayerType = "soundcloud";
8
+ var soundCloudWidgetPromise = null;
9
+ var elementStates = /* @__PURE__ */ new WeakMap();
10
+ var hostStates = /* @__PURE__ */ new WeakMap();
11
+ var getSoundCloudWindow = () => typeof window !== "undefined" && window.SC?.Widget ? window.SC : null;
12
+ var ensureSoundCloudWidgetApi = async () => {
13
+ const available = getSoundCloudWindow();
14
+ if (available) return available;
15
+ if (soundCloudWidgetPromise) return soundCloudWidgetPromise;
16
+ if (typeof document === "undefined") throw new Error("SoundCloud widget API requires a document context.");
17
+ soundCloudWidgetPromise = new Promise((resolve, reject) => {
18
+ const existingScript = document.querySelector(`script[src="${soundCloudWidgetApiUrl}"]`);
19
+ const script = existingScript ?? document.createElement("script");
20
+ const cleanup = () => {
21
+ script.removeEventListener("load", onLoad);
22
+ script.removeEventListener("error", onError);
23
+ };
24
+ const onError = () => {
25
+ cleanup();
26
+ soundCloudWidgetPromise = null;
27
+ reject(/* @__PURE__ */ new Error("Failed to load the SoundCloud widget API."));
28
+ };
29
+ const onLoad = () => {
30
+ cleanup();
31
+ const nextAvailable = getSoundCloudWindow();
32
+ if (!nextAvailable) {
33
+ soundCloudWidgetPromise = null;
34
+ reject(/* @__PURE__ */ new Error("SoundCloud widget API did not initialize."));
35
+ return;
36
+ }
37
+ resolve(nextAvailable);
38
+ };
39
+ script.addEventListener("load", onLoad, { once: true });
40
+ script.addEventListener("error", onError, { once: true });
41
+ if (!existingScript) {
42
+ script.async = true;
43
+ script.src = soundCloudWidgetApiUrl;
44
+ document.head.append(script);
45
+ }
46
+ });
47
+ return soundCloudWidgetPromise;
48
+ };
49
+ var getHostStateKey = (track) => `${track.type ?? soundCloudPlayerType}:${track.src}`;
50
+ var parseSoundCloudMetadata = (oembed) => {
51
+ const title = oembed.title?.replace(/\s+by\s+[^]+$/, "").trim();
52
+ const artist = oembed.author_name?.trim();
53
+ if (!title && !artist) return;
54
+ return {
55
+ artist,
56
+ title
57
+ };
58
+ };
59
+ var parseSoundCloudIframeSrc = (oembed) => {
60
+ if (!oembed.html || typeof document === "undefined") return null;
61
+ const template = document.createElement("template");
62
+ template.innerHTML = oembed.html;
63
+ return template.content.querySelector("iframe")?.getAttribute("src") ?? null;
64
+ };
65
+ var createHiddenIframe = (src) => {
66
+ const iframe = document.createElement("iframe");
67
+ iframe.src = src;
68
+ iframe.hidden = true;
69
+ iframe.tabIndex = -1;
70
+ iframe.title = "SoundCloud player";
71
+ iframe.setAttribute("aria-hidden", "true");
72
+ iframe.setAttribute("allow", "autoplay; encrypted-media");
73
+ iframe.setAttribute("data-jukette-soundcloud-iframe", "");
74
+ iframe.style.blockSize = "0";
75
+ iframe.style.border = "0";
76
+ iframe.style.inlineSize = "0";
77
+ iframe.style.opacity = "0";
78
+ iframe.style.pointerEvents = "none";
79
+ iframe.style.position = "absolute";
80
+ return iframe;
81
+ };
82
+ var getSoundCloudRoot = (host) => {
83
+ const rootNode = host.shadowRoot ?? host;
84
+ const existing = rootNode.querySelector(`[${soundCloudRootAttribute}]`);
85
+ if (existing) return existing;
86
+ const root = document.createElement("div");
87
+ root.hidden = true;
88
+ root.setAttribute(soundCloudRootAttribute, "");
89
+ root.style.display = "none";
90
+ rootNode.append(root);
91
+ return root;
92
+ };
93
+ var SoundCloudTrackState = class {
94
+ track;
95
+ host;
96
+ trackElement;
97
+ activeTrack = null;
98
+ durationSeconds = 0;
99
+ iframeElement = null;
100
+ metadata;
101
+ oEmbedPromise = null;
102
+ playRequested = false;
103
+ playing = false;
104
+ positionSeconds = 0;
105
+ ready = false;
106
+ widget = null;
107
+ widgetReadyPromise = null;
108
+ widgetReadyReject = null;
109
+ widgetReadyResolve = null;
110
+ constructor(track, host, trackElement) {
111
+ this.track = track;
112
+ this.host = host;
113
+ this.trackElement = trackElement;
114
+ }
115
+ get currentTime() {
116
+ return this.positionSeconds;
117
+ }
118
+ get duration() {
119
+ return this.durationSeconds;
120
+ }
121
+ attach(track) {
122
+ this.activeTrack = track;
123
+ }
124
+ detach(track) {
125
+ if (this.activeTrack === track) {
126
+ this.activeTrack = null;
127
+ this.playRequested = false;
128
+ this.playing = false;
129
+ }
130
+ }
131
+ async preload(options) {
132
+ if (!options.prepare && !options.preloadMetadata) return;
133
+ if (options.preloadMetadata || options.prepare) await this.ensureOEmbed();
134
+ if (options.prepare) await this.ensurePrepared();
135
+ return {
136
+ duration: this.durationSeconds || void 0,
137
+ metadata: this.metadata
138
+ };
139
+ }
140
+ async load(_options) {
141
+ this.playRequested = false;
142
+ this.playing = false;
143
+ this.activeTrack?.trackCallbacks.onStatus("Loading SoundCloud");
144
+ await this.ensureOEmbed();
145
+ await this.ensurePrepared();
146
+ this.widget?.pause();
147
+ this.seekTo(0);
148
+ this.activeTrack?.trackCallbacks.onMetadata(this.metadata ?? {});
149
+ if (this.durationSeconds > 0) this.activeTrack?.trackCallbacks.onDuration(this.durationSeconds);
150
+ this.activeTrack?.trackCallbacks.onProgress(this.positionSeconds, this.durationSeconds);
151
+ this.activeTrack?.trackCallbacks.onReady();
152
+ this.activeTrack?.trackCallbacks.onStatus();
153
+ }
154
+ async play(options) {
155
+ await this.ensurePrepared();
156
+ if (options.isStale()) return false;
157
+ if (!this.widget) return false;
158
+ if (options.restart || this.durationSeconds > 0 && this.positionSeconds >= this.durationSeconds) this.seekTo(0);
159
+ this.playRequested = true;
160
+ this.activeTrack?.trackCallbacks.onStatus("Starting SoundCloud");
161
+ this.widget.play();
162
+ return false;
163
+ }
164
+ pause() {
165
+ this.playRequested = false;
166
+ this.playing = false;
167
+ this.widget?.pause();
168
+ }
169
+ seekTo(seconds) {
170
+ const safeSeconds = Math.max(0, seconds);
171
+ this.positionSeconds = safeSeconds;
172
+ this.widget?.seekTo(Math.round(safeSeconds * 1e3));
173
+ }
174
+ async requestPosition() {
175
+ if (!this.widget) return;
176
+ const [position, duration] = await Promise.all([new Promise((resolve) => this.widget?.getPosition((value) => resolve(value / 1e3))), new Promise((resolve) => this.widget?.getDuration((value) => resolve(value / 1e3)))]);
177
+ if (duration > 0) this.durationSeconds = duration;
178
+ if (position >= 0) this.positionSeconds = position;
179
+ this.activeTrack?.trackCallbacks.onProgress(this.positionSeconds, this.durationSeconds);
180
+ }
181
+ async ensureOEmbed() {
182
+ if (this.oEmbedPromise) return this.oEmbedPromise;
183
+ if (typeof fetch === "undefined") throw new Error("SoundCloud oEmbed requires fetch support.");
184
+ const params = new URLSearchParams({
185
+ auto_play: "false",
186
+ buying: "false",
187
+ download: "false",
188
+ format: "json",
189
+ maxheight: "166",
190
+ sharing: "false",
191
+ show_artwork: "false",
192
+ show_comments: "false",
193
+ show_playcount: "false",
194
+ show_user: "true",
195
+ url: this.track.src
196
+ });
197
+ this.oEmbedPromise = fetch(`${soundCloudOEmbedUrl}?${params.toString()}`).then(async (response) => {
198
+ if (!response.ok) throw new Error("SoundCloud oEmbed request failed.");
199
+ return await response.json();
200
+ }).then((oembed) => {
201
+ this.metadata = parseSoundCloudMetadata(oembed);
202
+ return oembed;
203
+ });
204
+ return this.oEmbedPromise;
205
+ }
206
+ async ensurePrepared() {
207
+ if (this.ready) return;
208
+ const api = await ensureSoundCloudWidgetApi();
209
+ const iframeSrc = parseSoundCloudIframeSrc(await this.ensureOEmbed());
210
+ if (!iframeSrc) throw new Error("SoundCloud oEmbed did not include an iframe.");
211
+ if (!this.iframeElement) {
212
+ this.iframeElement = createHiddenIframe(iframeSrc);
213
+ getSoundCloudRoot(this.host).append(this.iframeElement);
214
+ }
215
+ if (!this.widget) {
216
+ this.widget = api.Widget(this.iframeElement);
217
+ this.widgetReadyPromise = new Promise((resolve, reject) => {
218
+ this.widgetReadyResolve = resolve;
219
+ this.widgetReadyReject = reject;
220
+ });
221
+ this.bindWidgetEvents(api.Widget.Events);
222
+ }
223
+ await this.widgetReadyPromise;
224
+ await this.readDuration();
225
+ }
226
+ bindWidgetEvents(events) {
227
+ if (!this.widget) return;
228
+ this.widget.bind(events.READY, () => {
229
+ this.ready = true;
230
+ this.widgetReadyResolve?.();
231
+ });
232
+ this.widget.bind(events.PLAY_PROGRESS, (payload) => {
233
+ if (!this.playing) return;
234
+ const position = (payload?.currentPosition ?? 0) / 1e3;
235
+ if (Number.isFinite(position)) this.positionSeconds = position;
236
+ this.activeTrack?.trackCallbacks.onProgress(this.positionSeconds, this.durationSeconds);
237
+ });
238
+ this.widget.bind(events.PLAY, () => {
239
+ if (!this.playRequested && !this.playing) return;
240
+ this.playing = true;
241
+ this.activeTrack?.handlePlayEvent();
242
+ });
243
+ this.widget.bind(events.PAUSE, () => {
244
+ this.playRequested = false;
245
+ this.playing = false;
246
+ this.activeTrack?.handlePauseEvent();
247
+ });
248
+ this.widget.bind(events.FINISH, () => {
249
+ this.playRequested = false;
250
+ this.playing = false;
251
+ this.positionSeconds = this.durationSeconds;
252
+ this.activeTrack?.trackCallbacks.onFinish();
253
+ });
254
+ this.widget.bind(events.ERROR, () => {
255
+ this.widgetReadyReject?.(/* @__PURE__ */ new Error("SoundCloud widget reported an error."));
256
+ this.activeTrack?.trackCallbacks.onStatus("SoundCloud playback failed");
257
+ });
258
+ }
259
+ async readDuration() {
260
+ if (!this.widget) return;
261
+ const duration = await new Promise((resolve) => this.widget?.getDuration((value) => resolve(value / 1e3)));
262
+ if (duration > 0) {
263
+ this.durationSeconds = duration;
264
+ this.activeTrack?.trackCallbacks.onDuration(this.durationSeconds);
265
+ }
266
+ }
267
+ };
268
+ var SoundCloudPlayableTrack = class extends JukettePlayableTrack {
269
+ state;
270
+ ignoreNextPauseEvent = false;
271
+ constructor(track, callbacks, state) {
272
+ super(track, callbacks);
273
+ this.state = state;
274
+ this.state.attach(this);
275
+ }
276
+ get trackCallbacks() {
277
+ return this.callbacks;
278
+ }
279
+ get currentTime() {
280
+ return this.state.currentTime;
281
+ }
282
+ get duration() {
283
+ return this.state.duration;
284
+ }
285
+ load(options) {
286
+ return this.state.load(options);
287
+ }
288
+ play(options) {
289
+ return this.state.play(options);
290
+ }
291
+ pause(_options = {}) {
292
+ this.ignoreNextPauseEvent = true;
293
+ this.state.pause();
294
+ }
295
+ seek(seconds) {
296
+ this.state.seekTo(seconds);
297
+ this.callbacks.onProgress(seconds, this.duration);
298
+ }
299
+ stop() {
300
+ this.pause({ silent: true });
301
+ this.state.detach(this);
302
+ }
303
+ requestPosition() {
304
+ this.state.requestPosition();
305
+ }
306
+ handlePauseEvent() {
307
+ if (this.ignoreNextPauseEvent) {
308
+ this.ignoreNextPauseEvent = false;
309
+ return;
310
+ }
311
+ this.callbacks.onPause();
312
+ }
313
+ handlePlayEvent() {
314
+ this.callbacks.onStatus();
315
+ this.callbacks.onPlay();
316
+ }
317
+ };
318
+ var createSoundCloudTrackState = (track, host, trackElement) => {
319
+ if (trackElement) {
320
+ const current = elementStates.get(trackElement);
321
+ if (current) return current;
322
+ const next = new SoundCloudTrackState(track, host, trackElement);
323
+ elementStates.set(trackElement, next);
324
+ return next;
325
+ }
326
+ let states = hostStates.get(host);
327
+ if (!states) {
328
+ states = /* @__PURE__ */ new Map();
329
+ hostStates.set(host, states);
330
+ }
331
+ const key = getHostStateKey(track);
332
+ const current = states.get(key);
333
+ if (current) return current;
334
+ const next = new SoundCloudTrackState(track, host, null);
335
+ states.set(key, next);
336
+ return next;
337
+ };
338
+ var soundCloudBackend = {
339
+ createPlayableTrack(track, callbacks, options) {
340
+ return new SoundCloudPlayableTrack(track, callbacks, createSoundCloudTrackState(track, options.host, options.trackElement));
341
+ },
342
+ preloadTrack: async (track, options) => createSoundCloudTrackState(track, options.host, options.trackElement).preload(options),
343
+ type: soundCloudPlayerType
344
+ };
345
+ var register = () => registerJuketteBackend(soundCloudBackend);
346
+ //#endregion
347
+ //#region src/lib/soundcloud-auto.ts
348
+ register();
349
+ //#endregion