@videojs/soundcloud-audio 0.0.0-stage → 10.3.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/LICENSE +20 -0
- package/README.md +24 -2
- package/dist/default/adapter.js +733 -0
- package/dist/default/index.js +3 -0
- package/dist/default/source.js +53 -0
- package/dist/default/widget-api.js +33 -0
- package/dist/dev/adapter.d.ts +82 -0
- package/dist/dev/adapter.js +735 -0
- package/dist/dev/adapter.js.map +1 -0
- package/dist/dev/index.d.ts +4 -0
- package/dist/dev/index.js +3 -0
- package/dist/dev/props.d.ts +13 -0
- package/dist/dev/source.d.ts +82 -0
- package/dist/dev/source.js +55 -0
- package/dist/dev/source.js.map +1 -0
- package/dist/dev/widget-api.js +35 -0
- package/dist/dev/widget-api.js.map +1 -0
- package/package.json +48 -3
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"adapter.js","names":["parseSoundCloudSource"],"sources":["../../src/adapter.ts"],"sourcesContent":["// Speaks the SoundCloud Widget protocol directly rather than loading `api.js`, shaped like the other iframe adapters\n// (mirrors `playerjs-video`).\n\nimport { EMPTY_TEXT_TRACKS, type MediaContentData, MediaError, NO_TIME_RANGES_YET, type Video } from '@videojs/media';\nimport { createTimeRange, MediaPlayedRangesMixin } from '@videojs/media/dom';\nimport { tryCall } from '@videojs/utils/function';\nimport { clamp } from '@videojs/utils/number';\nimport { deepEqual } from '@videojs/utils/object';\nimport { isNumber, isObject, isString, isUndefined } from '@videojs/utils/predicate';\n\nimport type { SoundCloudAdapterProps } from './props';\nimport {\n buildSoundCloudWidgetUrl,\n type ParsedSoundCloudSource,\n parseSoundCloudSource,\n type SoundCloudSource,\n wantsSoundCloudAutoplay,\n} from './source';\nimport {\n createSoundCloudCommand,\n isSoundCloudProgress,\n loadedShareOf,\n parseSoundCloudMessage,\n type SoundCloudProgress,\n type SoundCloudWidgetCommand,\n type SoundCloudWidgetEvent,\n type SoundCloudWidgetGetter,\n WIDGET_ORIGIN,\n} from './widget-api';\n\n/**\n * Plays SoundCloud tracks and sets through the SoundCloud Widget. A set plays as one source: moving to another track\n * reports a new `duration` and restarts `currentTime`, and `ended` fires once the last track ends. The widget stays\n * visible and takes input, since it is what credits the uploader and SoundCloud; what it does is reported like anything\n * the host asked for.\n *\n * @fires sourcechange - Fired when `source` changes, either directly or by resolving a new `src`. Read `source` for the\n * new value.\n * @fires contentdatachange - Fired when the widget reports the title and artwork of a track, and when they are cleared.\n * Read `contentData` for them.\n */\nexport class SoundCloudAdapter extends MediaPlayedRangesMixin(EventTarget) implements Partial<Video> {\n static readonly defaultProps: SoundCloudAdapterProps = {\n src: '',\n autoplay: false,\n defaultMuted: false,\n muted: false,\n loop: false,\n controls: false,\n playsInline: true,\n preload: 'metadata',\n poster: '',\n source: null,\n };\n\n static PLAYER_SOFTWARE_NAME = 'soundcloud-audio';\n\n #target: HTMLIFrameElement | null = null;\n // Cancels the `message` and frame `load` listeners; the first lives on `window`, so nothing removes it with the frame.\n #listeners: AbortController | null = null;\n // Guards messages across attach/detach cycles.\n #attachId = 0;\n // Lets a `load()` waiting out its microtask drop out once a newer load begins.\n #loadId = 0;\n // An iframe URL rendered before the host had a source, which the first load keeps instead of reloading.\n #adoptedSrc: string | null = null;\n\n #src = SoundCloudAdapter.defaultProps.src;\n #autoplay = SoundCloudAdapter.defaultProps.autoplay;\n #defaultMuted = SoundCloudAdapter.defaultProps.defaultMuted;\n #loop = SoundCloudAdapter.defaultProps.loop;\n #controls = SoundCloudAdapter.defaultProps.controls;\n #playsInline = SoundCloudAdapter.defaultProps.playsInline;\n #preload = SoundCloudAdapter.defaultProps.preload;\n #poster = SoundCloudAdapter.defaultProps.poster;\n #source: SoundCloudSource | null = SoundCloudAdapter.defaultProps.source;\n\n #parsed: ParsedSoundCloudSource | null = null;\n // Whether the frame has loaded the document last pointed at; until then, a getter answer may be the outgoing one's.\n #frameLoaded = true;\n #widgetId: string | null = null;\n // When the frame was last pointed at a widget; a document created before then is one the host moved away from.\n #navigatedAt = 0;\n // The document last replaced, for a widget id that carries no creation time.\n #staleWidgetId: string | null = null;\n #ready = false;\n #requests = new Map<SoundCloudWidgetGetter, ((value: unknown) => void)[]>();\n #playRequested = false;\n #autoplayPending = false;\n #startTime: number | null = null;\n #soundIds: readonly number[] = [];\n #soundId: number | null = null;\n // The widget rewinds to the start once the last track ends; the media keeps reporting the end until it plays again.\n #finished = false;\n // Playback was reported, but the position has not moved yet.\n #awaitingProgress = false;\n #playFrom = 0;\n #reportedTime = 0;\n #endTimer: ReturnType<typeof setTimeout> | undefined;\n #durationTimer: ReturnType<typeof setTimeout> | undefined;\n #seekTimer: ReturnType<typeof setTimeout> | undefined;\n\n #paused = true;\n #ended = false;\n #seeking = false;\n #currentTime = 0;\n #duration = Number.NaN;\n #bufferedStart = 0;\n #bufferedEnd = 0;\n // Volume and mute survive a source change, as on a media element, and are asserted on each new widget.\n #volume = 1;\n #muted = false;\n #readyState = READY_STATE_HAVE_NOTHING;\n #error: MediaError | null = null;\n #contentData: MediaContentData = {};\n\n /** The widget's window, which commands are posted to. Null until rendered. */\n get engine(): Window | null {\n return this.#target?.contentWindow ?? null;\n }\n\n get target(): HTMLIFrameElement | null {\n return this.#target;\n }\n\n /** Bind the iframe hosting the widget. The widget follows once a `src` resolves, which may be after attach. */\n attach(target: HTMLIFrameElement | null): void {\n if (!target || this.#target === target) return;\n\n if (this.#target) this.detach();\n\n this.#target = target;\n this.#listen(target);\n this.#createPlayer(target);\n }\n\n detach(): void {\n if (!this.#target) return;\n\n this.#attachId++;\n this.#loadId++;\n this.#listeners?.abort();\n this.#listeners = null;\n this.#target = null;\n this.#adoptedSrc = null;\n this.#playRequested = false;\n this.#resetState();\n }\n\n override destroy() {\n this.detach();\n super.destroy();\n }\n\n get src() {\n return this.#src;\n }\n /** SoundCloud URL. Setting it re-derives `source`, carrying its engine options over. */\n set src(value) {\n const { engine } = this.#source ?? {};\n const next: SoundCloudSource = { ...(engine && { engine }), ...(value && { src: value }) };\n\n // The `source` setter is the one path for storing it, deciding on a load, and dispatching `sourcechange`.\n this.source = Object.keys(next).length > 0 ? next : null;\n }\n\n get currentSrc() {\n // The `src` property resolves an empty attribute to the document URL, so only the attribute reports empty.\n return this.#target?.getAttribute('src') ?? '';\n }\n\n get readyState() {\n return this.#readyState;\n }\n\n /** Point the widget at the current source; rewriting the iframe URL is the only load the widget takes parameters on. */\n async load() {\n if (!this.#target) return;\n\n const loadId = ++this.#loadId;\n\n // Wait a microtask so a framework's `src` and prop writes all land before the URL is built, once.\n await Promise.resolve();\n\n const target = this.#target;\n if (loadId !== this.#loadId || !target) return;\n\n const adopted = this.#adoptedSrc;\n\n this.#adoptedSrc = null;\n\n // A widget rendered from the same source before the host had one is this load already.\n if (adopted && adopted === this.#buildWidgetUrl() && this.#hasReferrerPolicy(target)) {\n this.#parsed = parseSoundCloudSource(this.#src);\n this.#startTime = this.#parsed?.startTime ?? null;\n this.#autoplayPending = true;\n return;\n }\n\n this.#resetState();\n // `emptied` announces that reset before the bails below, where the widget reports nothing further.\n this.dispatchEvent(new Event('emptied'));\n this.#loadWidget(target);\n }\n\n get paused() {\n return this.#paused;\n }\n\n get ended() {\n return this.#ended;\n }\n\n get seeking() {\n return this.#seeking;\n }\n\n async play() {\n if (!this.#src || this.#error) return;\n\n if (!this.#ready) {\n this.#playRequested = true;\n return;\n }\n\n // A finished set stays on its last track; a media element plays from the start.\n if (this.#ended && this.#soundIds.length > 1 && this.#soundId !== this.#soundIds[0]) this.#post('skip', 0);\n else this.#post('play');\n }\n\n pause() {\n this.#playRequested = false;\n this.#autoplayPending = false;\n\n if (this.#ready) this.#post('pause');\n }\n\n get currentTime() {\n return this.#currentTime;\n }\n set currentTime(value) {\n const time = this.#clampTime(value);\n\n // With no source, or one that failed to load, no widget will ever report the seek landing.\n if (!this.#ready && (!this.#src || this.#error)) {\n this.#currentTime = time;\n return;\n }\n\n if (time === this.#currentTime && !this.#ended) return;\n\n this.#finished = false;\n this.#ended = false;\n this.#seeking = true;\n // Report the target now; the widget's reports until it lands would drag it back.\n this.#currentTime = time;\n this.#reportedTime = time;\n this.dispatchEvent(new Event('seeking'));\n this.dispatchEvent(new Event('timeupdate'));\n\n // Before ready, `#syncToWidget` sends the seek still pending.\n if (this.#ready) this.#seek(time);\n }\n\n get duration() {\n return this.#duration;\n }\n\n get volume() {\n return this.#volume;\n }\n set volume(value) {\n const volume = clamp(value, 0, 1);\n if (volume === this.#volume) return;\n\n this.#volume = volume;\n // The widget reports no volume changes, so the host announces its own.\n this.dispatchEvent(new Event('volumechange'));\n\n // While muted the widget holds `0`; the new level applies on unmute.\n if (!this.#muted) this.#applyVolume();\n }\n\n get muted() {\n return this.#muted;\n }\n set muted(value) {\n if (this.#muted === value) return;\n\n this.#muted = value;\n this.dispatchEvent(new Event('volumechange'));\n this.#applyVolume();\n }\n\n /** Starts playback once the widget is ready, after the volume and start position apply. Browsers may refuse it. */\n get autoplay() {\n return this.#autoplay;\n }\n set autoplay(value) {\n this.#autoplay = value;\n }\n\n get defaultMuted() {\n return this.#defaultMuted;\n }\n set defaultMuted(value) {\n this.#defaultMuted = value;\n\n // Seeds `muted` until the widget is up; `#syncToWidget` then applies it.\n if (!this.#ready) this.#muted = value;\n }\n\n /** Replays the track, or a set from its first track, when the last one ends. */\n get loop() {\n return this.#loop;\n }\n set loop(value) {\n this.#loop = value;\n }\n\n get controls() {\n return this.#controls;\n }\n set controls(value) {\n this.#controls = value;\n }\n\n get playsInline() {\n return this.#playsInline;\n }\n set playsInline(value) {\n this.#playsInline = value;\n }\n\n get preload() {\n return this.#preload;\n }\n set preload(value) {\n this.#preload = value;\n }\n\n get poster() {\n return this.#poster;\n }\n set poster(value) {\n this.#poster = value;\n }\n\n /** SoundCloud URL in `src`, widget parameters under `engine.soundcloud`. Replacing it re-derives `src`. */\n get source(): SoundCloudSource | null {\n return this.#source;\n }\n set source(value: SoundCloudSource | null) {\n const source = value ?? null;\n if (source === this.#source) return;\n\n const src = source?.src ?? '';\n const srcChanged = this.#src !== src;\n // Widget parameters are read when the widget is built, so a change needs its own reload even for the same source.\n const engineChanged = !deepEqual(this.#source?.engine?.soundcloud ?? null, source?.engine?.soundcloud ?? null);\n\n this.#source = source;\n this.#src = src;\n\n if (srcChanged || engineChanged) void this.load();\n\n this.dispatchEvent(new Event('sourcechange'));\n }\n\n /** The stretch the widget reports loaded, from where playback last started or landed. */\n get buffered() {\n return this.#bufferedEnd > this.#bufferedStart\n ? createTimeRange(this.#bufferedStart, this.#bufferedEnd)\n : NO_TIME_RANGES_YET;\n }\n\n get seekable() {\n return this.#duration > 0 && Number.isFinite(this.#duration)\n ? createTimeRange(0, this.#duration)\n : NO_TIME_RANGES_YET;\n }\n\n get error() {\n return this.#error;\n }\n\n /**\n * The `title` and `poster` of the track playing, which change with each track of a set. Empty until the widget\n * reports them, and again across a source change. `contentdatachange` announces both.\n */\n get contentData(): MediaContentData {\n return this.#contentData;\n }\n\n /** Always empty: SoundCloud has no captions or other text tracks. */\n get textTracks() {\n return EMPTY_TEXT_TRACKS;\n }\n\n // Playback rate, picture-in-picture, and fullscreen are left out on purpose: the widget offers none of them, and a\n // member that cannot reach the player would read as a capability it has.\n\n // A rendered frame is kept and its `ready` settles the load; an empty one is pointed at the current source.\n #createPlayer(target: HTMLIFrameElement) {\n const rendered = target.getAttribute('src');\n\n if (!rendered) {\n if (this.#src) this.#loadWidget(target);\n\n return;\n }\n\n // A widget that loaded before anything listened has already broadcast its `ready`.\n this.#frameLoaded = hasWidgetDocument(target);\n this.#navigatedAt = 0;\n this.#staleWidgetId = null;\n this.dispatchEvent(new Event('loadstart'));\n this.#probeReady();\n this.#adoptedSrc = rendered;\n\n if (this.#src) void this.load();\n }\n\n #loadWidget(target: HTMLIFrameElement) {\n const parsed = parseSoundCloudSource(this.#src);\n\n this.#parsed = parsed;\n this.#startTime = parsed?.startTime ?? null;\n\n if (!this.#src) {\n // Left in place, the widget would keep playing a source the host no longer tracks.\n target.removeAttribute('src');\n return;\n }\n\n this.#autoplayPending = true;\n this.dispatchEvent(new Event('loadstart'));\n\n if (!parsed) {\n target.removeAttribute('src');\n this.#fail(\n new MediaError(`Unrecognized SoundCloud source: ${this.#src}`, MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED)\n );\n return;\n }\n\n this.#navigate(target, buildSoundCloudWidgetUrl(parsed.url, parsed, this.#snapshotProps()));\n }\n\n #navigate(target: HTMLIFrameElement, url: string) {\n const referrerPolicy = this.#source?.engine?.soundcloud?.referrerPolicy;\n\n // A frame sends the policy it has when it navigates, so it goes on first.\n if (referrerPolicy !== undefined) target.referrerPolicy = referrerPolicy;\n\n this.#frameLoaded = false;\n this.#navigatedAt = Date.now();\n target.src = url;\n }\n\n #hasReferrerPolicy(target: HTMLIFrameElement): boolean {\n const referrerPolicy = this.#source?.engine?.soundcloud?.referrerPolicy;\n\n return referrerPolicy === undefined || target.referrerPolicy === referrerPolicy;\n }\n\n #buildWidgetUrl(): string {\n const parsed = parseSoundCloudSource(this.#src);\n if (!parsed) return '';\n\n return buildSoundCloudWidgetUrl(parsed.url, parsed, this.#snapshotProps());\n }\n\n #listen(target: HTMLIFrameElement) {\n const win = globalThis.window;\n if (isUndefined(win)) return;\n\n const attachId = this.#attachId;\n\n this.#listeners = new AbortController();\n\n const { signal } = this.#listeners;\n\n win.addEventListener('message', (event) => this.#onMessage(event, target, attachId), { signal });\n target.addEventListener(\n 'load',\n () => {\n this.#frameLoaded = hasWidgetDocument(target);\n\n if (!this.#ready) this.#probeReady();\n },\n { signal }\n );\n }\n\n #onMessage(event: MessageEvent, target: HTMLIFrameElement, attachId: number) {\n if (attachId !== this.#attachId || event.origin !== WIDGET_ORIGIN) return;\n\n const frame = target.contentWindow;\n if (!frame || event.source !== frame) return;\n\n const message = parseSoundCloudMessage(event.data);\n if (!message) return;\n\n const { method, value, widgetId = null } = message;\n\n if (!this.#ready) {\n // The widget broadcasts `ready` once per document; a frame that loaded before anything listened answers the\n // probe instead.\n const isReady = method === 'ready' || (method === READY_PROBE && this.#frameLoaded);\n\n if (isReady && this.#isCurrentDocument(widgetId)) this.#onReady(widgetId);\n\n return;\n }\n\n if (method === 'ready' && widgetId !== this.#widgetId && this.#isCurrentDocument(widgetId)) {\n this.#onFrameReloaded(widgetId);\n return;\n }\n\n if (widgetId !== this.#widgetId) return;\n\n const answer = this.#requests.get(method as SoundCloudWidgetGetter)?.shift();\n\n if (answer) {\n answer(value);\n return;\n }\n\n switch (method as SoundCloudWidgetEvent) {\n case 'play':\n if (isSoundCloudProgress(value)) this.#onWidgetPlay(value);\n\n break;\n case 'pause':\n if (isSoundCloudProgress(value)) this.#onWidgetPause(value);\n\n break;\n case 'finish':\n if (isSoundCloudProgress(value)) this.#onWidgetFinish(value);\n\n break;\n case 'seek':\n if (isSoundCloudProgress(value)) this.#onWidgetSeek(value);\n\n break;\n case 'playProgress':\n if (isSoundCloudProgress(value)) this.#onWidgetProgress(value);\n\n break;\n case 'loadProgress':\n if (isSoundCloudProgress(value)) this.#syncBuffered(value);\n\n break;\n case 'error':\n this.#onWidgetError();\n break;\n }\n }\n\n #onReady(widgetId: string | null) {\n this.#ready = true;\n this.#widgetId = widgetId;\n\n for (const type of SUBSCRIBED_EVENTS) this.#post('addEventListener', type);\n\n // Answers come in order, after the error the widget replays to a new `error` listener when it has one to show.\n this.#get('getSounds').then((sounds) => {\n this.#soundIds = Array.isArray(sounds) ? sounds.map(soundIdOf).filter(isNumber) : [];\n });\n this.#get('getCurrentSound').then((sound) => {\n this.#soundId = soundIdOf(sound) ?? null;\n this.#setContentData(sound);\n });\n this.#get('getDuration').then((duration) => this.#onLoaded(duration));\n }\n\n // The frame loaded a new widget without the host pointing it anywhere, as moving an iframe in the DOM does. The new\n // widget starts over, so the media does too.\n #onFrameReloaded(widgetId: string | null) {\n this.#resetState();\n this.#startTime = this.#parsed?.startTime ?? null;\n this.dispatchEvent(new Event('emptied'));\n this.dispatchEvent(new Event('loadstart'));\n this.#onReady(widgetId);\n }\n\n #onLoaded(duration: unknown, attempt = 1) {\n if (this.#error) return;\n\n if (!isNumber(duration) || !(duration > 0)) {\n // A set opened on a later track (`start_track`) answers `null` until that track loads; a sound the widget cannot\n // play reports `error` instead, which ends the wait. So a wait that runs out is a slow load, not a bad sound.\n if (attempt >= DURATION_ATTEMPTS) {\n this.#fail(\n new MediaError('The SoundCloud widget did not finish loading in time.', MediaError.MEDIA_ERR_NETWORK)\n );\n return;\n }\n\n const widgetId = this.#widgetId;\n\n this.#durationTimer = setTimeout(() => {\n if (this.#ready && this.#widgetId === widgetId) {\n this.#get('getDuration').then((next) => this.#onLoaded(next, attempt + 1));\n }\n }, DURATION_RETRY_INTERVAL);\n return;\n }\n\n this.#applyDuration(duration / 1000);\n this.#readyState = READY_STATE_HAVE_ENOUGH_DATA;\n\n for (const type of ['loadedmetadata', 'loadeddata', 'canplay', 'canplaythrough', 'loadcomplete']) {\n this.dispatchEvent(new Event(type));\n }\n\n this.#syncToWidget();\n\n // A widget rendered before the host listened may already be playing.\n this.#get('isPaused').then((paused) => {\n if (paused === false && this.#paused) this.#onWidgetPlay({ currentPosition: this.#currentTime * 1000 });\n });\n }\n\n // Apply what was asked for before the widget was ready. The volume goes first, so a muted autoplay starts muted.\n #syncToWidget() {\n this.#applyVolume();\n\n if (this.#seeking) {\n this.#currentTime = this.#clampTime(this.#currentTime);\n this.#seek(this.#currentTime);\n } else if (isNumber(this.#startTime) && this.#startTime > 0) this.currentTime = this.#startTime;\n\n const autoplay = this.#autoplayPending && (this.#autoplay || wantsSoundCloudAutoplay(this.#parsed, this.#source));\n\n if (this.#playRequested || autoplay) this.#post('play');\n\n this.#playRequested = false;\n this.#autoplayPending = false;\n }\n\n #onWidgetPlay(progress: SoundCloudProgress) {\n this.#syncSound(progress);\n this.#finished = false;\n clearTimeout(this.#endTimer);\n\n if (!this.#paused) return;\n\n const position = (progress.currentPosition ?? 0) / 1000;\n const loaded = (loadedShareOf(progress) ?? 0) * this.#duration;\n\n this.#paused = false;\n this.#ended = false;\n this.dispatchEvent(new Event('play'));\n\n if (loaded > position + STARVED_MARGIN) {\n this.dispatchEvent(new Event('playing'));\n return;\n }\n\n // Nothing ahead of the position is loaded yet; the position moving says it is.\n this.#awaitingProgress = true;\n this.#playFrom = position;\n this.#readyState = READY_STATE_HAVE_CURRENT_DATA;\n this.dispatchEvent(new Event('waiting'));\n }\n\n #onWidgetPause(progress: SoundCloudProgress) {\n this.#syncSound(progress);\n\n // The widget pauses at the end of every track; whether that is the end of playback is for `finish` to say.\n if (this.#isAtEnd(progress)) {\n clearTimeout(this.#endTimer);\n this.#endTimer = setTimeout(() => this.#onPause(), END_GRACE);\n return;\n }\n\n this.#onPause();\n }\n\n #onWidgetFinish(progress: SoundCloudProgress) {\n clearTimeout(this.#endTimer);\n this.#syncSound(progress);\n\n const index = this.#soundId === null ? -1 : this.#soundIds.indexOf(this.#soundId);\n const isLast = this.#soundIds.length === 0 || index === this.#soundIds.length - 1;\n\n if (!isLast) {\n // A set moves on to its next track by itself, which the next `play` reports; one it cannot play ends playback.\n this.#endTimer = setTimeout(() => this.#end(), END_GRACE);\n return;\n }\n\n if (!this.#loop) {\n this.#end();\n return;\n }\n\n this.#applyCurrentTime(0, true);\n\n if (this.#soundIds.length > 1) this.#post('skip', 0);\n else this.#post('play');\n\n // A loop that does not start again is the end of playback after all.\n this.#endTimer = setTimeout(() => this.#end(), END_GRACE);\n }\n\n #onWidgetSeek(progress: SoundCloudProgress) {\n this.#syncSound(progress);\n\n if (this.#finished) return;\n\n const time = this.#clampTime((progress.currentPosition ?? 0) / 1000);\n\n // A seek loads from where it lands.\n this.#setBuffered(time, this.#loadedEnd(progress) ?? this.#bufferedEnd);\n\n if (this.#seeking) {\n this.#settleSeek(time);\n return;\n }\n\n // The widget repeats its position as seeks when it pauses.\n if (Math.abs(time - this.#currentTime) < SEEK_TOLERANCE) return;\n\n this.#ended = false;\n this.#seeking = true;\n this.dispatchEvent(new Event('seeking'));\n this.#settleSeek(time);\n }\n\n #onWidgetProgress(progress: SoundCloudProgress) {\n this.#syncSound(progress);\n\n if (this.#finished) return;\n\n this.#syncBuffered(progress);\n\n // Positions from before a seek lands would drag the slider back.\n if (this.#seeking) return;\n\n const time = (progress.currentPosition ?? 0) / 1000;\n\n this.#applyCurrentTime(time);\n\n if (this.#awaitingProgress && !this.#paused && time > this.#playFrom) {\n this.#awaitingProgress = false;\n this.#readyState = READY_STATE_HAVE_ENOUGH_DATA;\n this.dispatchEvent(new Event('playing'));\n }\n }\n\n #onWidgetError() {\n if (this.#readyState < READY_STATE_HAVE_METADATA) {\n this.#fail(\n new MediaError(\n `SoundCloud could not play ${this.#src}. It may be private, removed, or not available in this region.`,\n MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED\n )\n );\n this.#onPause();\n return;\n }\n\n // Announced but not kept: a set moves on to its next track by itself, and nothing else blocks the source.\n const error = new MediaError('The SoundCloud widget reported an error.', MediaError.MEDIA_ERR_CUSTOM, false);\n\n this.dispatchEvent(new ErrorEvent('error', { error, message: error.message }));\n this.#get('isPaused').then((paused) => {\n if (paused === true) this.#onPause();\n });\n }\n\n // A sound other than the current one is another track of the set, or a related track picked in the widget.\n #syncSound({ soundId }: SoundCloudProgress) {\n if (!isNumber(soundId) || soundId === this.#soundId) return;\n\n const changed = this.#soundId !== null;\n\n this.#soundId = soundId;\n\n if (!changed) return;\n\n clearTimeout(this.#endTimer);\n this.#finished = false;\n this.#ended = false;\n this.#setBuffered(0, 0);\n this.#applyCurrentTime(0, true);\n this.#get('getDuration').then((duration) => {\n if (isNumber(duration) && duration > 0) this.#applyDuration(duration / 1000);\n });\n this.#get('getCurrentSound').then((sound) => {\n if (soundIdOf(sound) === this.#soundId) this.#setContentData(sound);\n });\n }\n\n #end() {\n clearTimeout(this.#endTimer);\n this.#finished = true;\n this.#applyCurrentTime(this.#duration, true);\n this.#onPause();\n this.#ended = true;\n this.dispatchEvent(new Event('ended'));\n }\n\n #onPause() {\n clearTimeout(this.#endTimer);\n this.#awaitingProgress = false;\n\n if (this.#paused) return;\n\n this.#paused = true;\n this.dispatchEvent(new Event('pause'));\n }\n\n #seek(time: number) {\n this.#post('seekTo', Math.round(time * 1000));\n clearTimeout(this.#seekTimer);\n\n // The widget reports no seek it ignores, so a seek still open after a while settles where the widget is.\n this.#seekTimer = setTimeout(() => {\n this.#get('getPosition').then((position) => {\n if (this.#seeking && isNumber(position)) this.#settleSeek(this.#clampTime(position / 1000));\n });\n }, SEEK_SETTLE_TIMEOUT);\n }\n\n #settleSeek(time: number) {\n clearTimeout(this.#seekTimer);\n this.#applyCurrentTime(time, true);\n this.#seeking = false;\n this.dispatchEvent(new Event('seeked'));\n }\n\n #applyVolume() {\n if (!this.#ready) return;\n\n this.#post('setVolume', this.#muted ? 0 : Math.round(this.#volume * 100));\n }\n\n #applyDuration(duration: number) {\n if (duration === this.#duration) return;\n\n this.#duration = duration;\n this.dispatchEvent(new Event('durationchange'));\n }\n\n // The widget reports its position about 50 times a second, so `timeupdate` follows a media element's pace instead.\n #applyCurrentTime(time: number, force = false) {\n const clamped = this.#clampTime(time);\n if (clamped === this.#currentTime) return;\n\n this.#currentTime = clamped;\n\n if (!force && Math.abs(clamped - this.#reportedTime) < TIMEUPDATE_INTERVAL) return;\n\n this.#reportedTime = clamped;\n this.dispatchEvent(new Event('timeupdate'));\n }\n\n #syncBuffered(progress: SoundCloudProgress) {\n const end = this.#loadedEnd(progress);\n\n if (!isUndefined(end)) this.#setBuffered(this.#bufferedStart, end);\n }\n\n #loadedEnd(progress: SoundCloudProgress): number | undefined {\n const share = loadedShareOf(progress);\n\n return isUndefined(share) || !Number.isFinite(this.#duration) ? undefined : clamp(share, 0, 1) * this.#duration;\n }\n\n #setBuffered(start: number, end: number) {\n if (start === this.#bufferedStart && end === this.#bufferedEnd) return;\n\n this.#bufferedStart = start;\n this.#bufferedEnd = end;\n this.dispatchEvent(new Event('progress'));\n }\n\n #clampTime(time: number): number {\n const max = Number.isFinite(this.#duration) ? this.#duration : Number.POSITIVE_INFINITY;\n\n return clamp(Number.isFinite(time) ? time : 0, 0, max);\n }\n\n #isAtEnd({ currentPosition }: SoundCloudProgress): boolean {\n return Number.isFinite(this.#duration) && this.#duration - (currentPosition ?? 0) / 1000 <= END_TOLERANCE;\n }\n\n #setContentData(sound: unknown) {\n const contentData = contentDataOf(sound);\n if (deepEqual(contentData, this.#contentData)) return;\n\n this.#contentData = contentData;\n this.dispatchEvent(new Event('contentdatachange'));\n }\n\n #fail(error: MediaError) {\n this.#error = error;\n this.dispatchEvent(new Event('error'));\n }\n\n // Resolves `undefined` at once without a widget, and never for a load replaced before the widget answers.\n #get(method: SoundCloudWidgetGetter): Promise<unknown> {\n if (!this.#ready) return Promise.resolve(undefined);\n\n return new Promise((resolve) => {\n const queue = this.#requests.get(method) ?? [];\n\n queue.push(resolve);\n this.#requests.set(method, queue);\n this.#post(method);\n });\n }\n\n // A document that loses the race with a newer navigation can still report `ready`, or answer the probe once its\n // `load` fires late; only one created after the latest navigation plays what the host asked for.\n #isCurrentDocument(widgetId: string | null): boolean {\n if (widgetId === null) return true;\n\n const createdAt = Number(WIDGET_CREATED_AT.exec(widgetId)?.[1]);\n\n return Number.isFinite(createdAt) ? createdAt >= this.#navigatedAt : widgetId !== this.#staleWidgetId;\n }\n\n #probeReady() {\n if (this.#frameLoaded) this.#post(READY_PROBE);\n }\n\n // Posting to a frame being torn down throws and is swallowed.\n #post(method: SoundCloudWidgetCommand | SoundCloudWidgetGetter, value?: string | number) {\n const frame = this.#target?.contentWindow;\n if (!frame) return;\n\n tryCall(() => frame.postMessage(createSoundCloudCommand(method, value), WIDGET_ORIGIN));\n }\n\n #snapshotProps(): Partial<SoundCloudAdapterProps> {\n return { source: this.#source };\n }\n\n #resetState() {\n clearTimeout(this.#endTimer);\n clearTimeout(this.#seekTimer);\n clearTimeout(this.#durationTimer);\n this.#staleWidgetId = this.#widgetId ?? this.#staleWidgetId;\n this.#widgetId = null;\n this.#ready = false;\n this.#requests.clear();\n this.#autoplayPending = false;\n this.#startTime = null;\n this.#soundIds = [];\n this.#soundId = null;\n this.#finished = false;\n this.#awaitingProgress = false;\n this.#playFrom = 0;\n this.#reportedTime = 0;\n this.#paused = true;\n this.#ended = false;\n this.#seeking = false;\n this.#currentTime = 0;\n this.#duration = Number.NaN;\n this.#bufferedStart = 0;\n this.#bufferedEnd = 0;\n this.#readyState = READY_STATE_HAVE_NOTHING;\n this.#error = null;\n this.#setContentData(null);\n }\n}\n\n// The frame starts on a same-origin `about:blank`, which a post meant for the widget only warns about.\nfunction hasWidgetDocument(target: HTMLIFrameElement): boolean {\n return !!target.getAttribute('src') && target.contentDocument === null;\n}\n\nfunction soundIdOf(sound: unknown): number | undefined {\n const id = isObject(sound) ? (sound as { id?: unknown }).id : undefined;\n\n return isNumber(id) ? id : isString(id) && /^\\d+$/.test(id) ? Number(id) : undefined;\n}\n\n// An empty title or artwork is left out, so a player falls back to the one its author set.\nfunction contentDataOf(sound: unknown): MediaContentData {\n if (!isObject(sound)) return {};\n\n const { title, artwork_url: artwork, user } = sound as SoundCloudSound;\n const image = isString(artwork) && artwork ? artwork : user?.avatar_url;\n\n return {\n ...(isString(title) && title && { title }),\n // The widget reports a 100px image; SoundCloud serves the same one at 500px.\n ...(isString(image) && image && { poster: image.replace(ARTWORK_SIZE, '-t500x500.') }),\n };\n}\n\ninterface SoundCloudSound {\n title?: unknown;\n artwork_url?: unknown;\n user?: { avatar_url?: string };\n}\n\nconst ARTWORK_SIZE = /-large\\.(?=[a-z]+$)/;\n\nconst SUBSCRIBED_EVENTS: readonly SoundCloudWidgetEvent[] = [\n 'play',\n 'pause',\n 'finish',\n 'seek',\n 'playProgress',\n 'loadProgress',\n 'error',\n];\n\n// The widget names each document after `Date.now()` in the frame when it starts.\nconst WIDGET_CREATED_AT = /^widget_(\\d+)$/;\n\n// Only ever asked to find out whether a frame is ready, so its answers are never mistaken for a request's.\nconst READY_PROBE: SoundCloudWidgetGetter = 'getCurrentSoundIndex';\n\n// Seconds from the end inside which a pause is the widget stopping at the end of a track.\nconst END_TOLERANCE = 0.5;\n// How long the widget gets to report what follows the end of a track.\nconst END_GRACE = 1_500;\nconst SEEK_TOLERANCE = 0.05;\n// Seconds that have to be loaded ahead of the position for playback to start without waiting.\nconst STARVED_MARGIN = 0.5;\nconst SEEK_SETTLE_TIMEOUT = 2_000;\nconst DURATION_ATTEMPTS = 20;\nconst DURATION_RETRY_INTERVAL = 250;\nconst TIMEUPDATE_INTERVAL = 0.25;\n\nconst READY_STATE_HAVE_NOTHING = 0;\nconst READY_STATE_HAVE_METADATA = 1;\nconst READY_STATE_HAVE_CURRENT_DATA = 2;\nconst READY_STATE_HAVE_ENOUGH_DATA = 4;\n"],"mappings":"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"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { ParsedSoundCloudSource, SoundCloudEngineConfig, SoundCloudSource, SoundCloudSourceEngineConfig, SoundCloudSourceKind, SoundCloudWidgetParamValue, buildSoundCloudIframeSrc, buildSoundCloudWidgetUrl, parseSoundCloudSource, wantsSoundCloudAutoplay } from "./source.js";
|
|
2
|
+
import { SoundCloudAdapterProps } from "./props.js";
|
|
3
|
+
import { SoundCloudAdapter } from "./adapter.js";
|
|
4
|
+
export { type ParsedSoundCloudSource, SoundCloudAdapter, type SoundCloudAdapterProps, SoundCloudEngineConfig, SoundCloudSource, SoundCloudSourceEngineConfig, type SoundCloudSourceKind, SoundCloudWidgetParamValue, buildSoundCloudIframeSrc, buildSoundCloudWidgetUrl, parseSoundCloudSource, wantsSoundCloudAutoplay };
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { buildSoundCloudIframeSrc, buildSoundCloudWidgetUrl, parseSoundCloudSource, wantsSoundCloudAutoplay } from "./source.js";
|
|
2
|
+
import { SoundCloudAdapter } from "./adapter.js";
|
|
3
|
+
export { SoundCloudAdapter, buildSoundCloudIframeSrc, buildSoundCloudWidgetUrl, parseSoundCloudSource, wantsSoundCloudAutoplay };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { SoundCloudSource } from "./source.js";
|
|
2
|
+
import { Video } from "@videojs/media";
|
|
3
|
+
//#region src/props.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* The `Video` members the SoundCloud host accepts, plus the source that names the track or set. The widget is the only
|
|
6
|
+
* UI SoundCloud offers, so `controls` decides whether the host shows it at all. `preload`, `playsInline`, and `poster`
|
|
7
|
+
* are kept for a uniform prop shape and stored without effect.
|
|
8
|
+
*/
|
|
9
|
+
export interface SoundCloudAdapterProps extends Pick<Video, 'src' | 'autoplay' | 'defaultMuted' | 'muted' | 'loop' | 'controls' | 'playsInline' | 'preload' | 'poster'> {
|
|
10
|
+
source: SoundCloudSource | null;
|
|
11
|
+
}
|
|
12
|
+
//#endregion
|
|
13
|
+
//# sourceMappingURL=props.d.ts.map
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { SoundCloudAdapterProps } from "./props.js";
|
|
2
|
+
import { ParsedSoundCloudSource, ParsedSoundCloudSource as ParsedSoundCloudSource$1, SoundCloudSourceKind, parseSoundCloudSource as parseSoundCloudSource$1 } from "@videojs/media";
|
|
3
|
+
//#region src/source.d.ts
|
|
4
|
+
/** What a widget parameter can be given as. `null` removes a parameter the embed URL would otherwise carry. */
|
|
5
|
+
export type SoundCloudWidgetParamValue = string | number | boolean | null | undefined;
|
|
6
|
+
/**
|
|
7
|
+
* SoundCloud Widget parameters, spelled as SoundCloud spells them
|
|
8
|
+
* (https://developers.soundcloud.com/docs/api/html5-widget#parameters) and written onto the widget URL. They win over
|
|
9
|
+
* the parameters of a `w.soundcloud.com/player/` URL given as `src`, which win over the host's defaults. Anything not
|
|
10
|
+
* listed still passes through, so parameters SoundCloud adds later keep working.
|
|
11
|
+
*
|
|
12
|
+
* The host's defaults hide the social chrome a player skin has no use for: `buying`, `sharing`, `download`,
|
|
13
|
+
* `show_comments`, `show_reposts`, and `show_teaser` are `false`, `hide_related` is `true`, and `visual` is `false`.
|
|
14
|
+
* The uploader and SoundCloud's own branding stay whenever the widget shows: the widget is what credits the sound.
|
|
15
|
+
*/
|
|
16
|
+
export interface SoundCloudEngineConfig extends Record<string, SoundCloudWidgetParamValue> {
|
|
17
|
+
/** Hex color of the play button and other accents, such as `'#ff5500'`. */
|
|
18
|
+
color?: string | null;
|
|
19
|
+
/** The large artwork layout. Defaults to `false`. */
|
|
20
|
+
visual?: boolean | null;
|
|
21
|
+
/** Start playback once the widget is ready, as `autoplay` does. Never written to the URL. */
|
|
22
|
+
auto_play?: boolean | null;
|
|
23
|
+
/** Index of the track in a set to start from. */
|
|
24
|
+
start_track?: number | null;
|
|
25
|
+
/** When `false`, playing this widget does not pause the other SoundCloud widgets on the page. */
|
|
26
|
+
single_active?: boolean | null;
|
|
27
|
+
/** Show the artwork. */
|
|
28
|
+
show_artwork?: boolean | null;
|
|
29
|
+
/** Show the uploader's name. The widget is what credits the uploader, so keep it unless the page does that elsewhere. */
|
|
30
|
+
show_user?: boolean | null;
|
|
31
|
+
/** Show the play count. */
|
|
32
|
+
show_playcount?: boolean | null;
|
|
33
|
+
/** Show listener comments on the waveform. Defaults to `false`. */
|
|
34
|
+
show_comments?: boolean | null;
|
|
35
|
+
/** Show reposts. Defaults to `false`. */
|
|
36
|
+
show_reposts?: boolean | null;
|
|
37
|
+
/** Show SoundCloud's promotional teaser. Defaults to `false`. */
|
|
38
|
+
show_teaser?: boolean | null;
|
|
39
|
+
/** Hide the related tracks the widget offers once a track ends. Defaults to `true`. */
|
|
40
|
+
hide_related?: boolean | null;
|
|
41
|
+
/** Show the buy button. Defaults to `false`. */
|
|
42
|
+
buying?: boolean | null;
|
|
43
|
+
/** Show the share button. Defaults to `false`. */
|
|
44
|
+
sharing?: boolean | null;
|
|
45
|
+
/** Show the download button. Defaults to `false`. */
|
|
46
|
+
download?: boolean | null;
|
|
47
|
+
/** `referrerpolicy` for the widget iframe. Not a widget parameter. */
|
|
48
|
+
referrerPolicy?: ReferrerPolicy;
|
|
49
|
+
}
|
|
50
|
+
/** Structured SoundCloud source: which track or set to play, plus how to play it. */
|
|
51
|
+
export interface SoundCloudSource {
|
|
52
|
+
/** SoundCloud track, set, or widget embed URL. Mirrors the host's `src` property. */
|
|
53
|
+
src?: string | undefined;
|
|
54
|
+
/** Playback options, keyed by the engine that reads them. */
|
|
55
|
+
engine?: SoundCloudSourceEngineConfig | undefined;
|
|
56
|
+
}
|
|
57
|
+
/** The engines a SoundCloud source can configure. */
|
|
58
|
+
export interface SoundCloudSourceEngineConfig {
|
|
59
|
+
/** SoundCloud Widget parameters, passed through untouched. */
|
|
60
|
+
soundcloud?: SoundCloudEngineConfig | undefined;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Build the widget iframe URL for a source. Empty when `src` is not a SoundCloud URL.
|
|
64
|
+
*
|
|
65
|
+
* @internal
|
|
66
|
+
*/
|
|
67
|
+
export declare function buildSoundCloudIframeSrc(src: string, props?: Partial<SoundCloudAdapterProps>): string;
|
|
68
|
+
/**
|
|
69
|
+
* Build the widget iframe URL for a resource URL the widget plays, carrying the parameters of the source it came from.
|
|
70
|
+
*
|
|
71
|
+
* @internal
|
|
72
|
+
*/
|
|
73
|
+
export declare function buildSoundCloudWidgetUrl(resourceUrl: string, parsed: Pick<ParsedSoundCloudSource, 'params'>, props?: Partial<SoundCloudAdapterProps>): string;
|
|
74
|
+
/**
|
|
75
|
+
* Whether the source asks to autoplay through the widget's own `auto_play` parameter.
|
|
76
|
+
*
|
|
77
|
+
* @internal
|
|
78
|
+
*/
|
|
79
|
+
export declare function wantsSoundCloudAutoplay(parsed: Pick<ParsedSoundCloudSource, 'params'> | null, source?: SoundCloudSource | null): boolean;
|
|
80
|
+
//#endregion
|
|
81
|
+
export { type ParsedSoundCloudSource$1 as ParsedSoundCloudSource, type SoundCloudSourceKind, parseSoundCloudSource$1 as parseSoundCloudSource };
|
|
82
|
+
//# sourceMappingURL=source.d.ts.map
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { WIDGET_ORIGIN } from "./widget-api.js";
|
|
2
|
+
import { parseSoundCloudSource, parseSoundCloudSource as parseSoundCloudSource$1 } from "@videojs/media";
|
|
3
|
+
//#region src/source.ts
|
|
4
|
+
/**
|
|
5
|
+
* Build the widget iframe URL for a source. Empty when `src` is not a SoundCloud URL.
|
|
6
|
+
*
|
|
7
|
+
* @internal
|
|
8
|
+
*/
|
|
9
|
+
function buildSoundCloudIframeSrc(src, props = {}) {
|
|
10
|
+
const parsed = parseSoundCloudSource(src);
|
|
11
|
+
if (!parsed) return "";
|
|
12
|
+
return buildSoundCloudWidgetUrl(parsed.url, parsed, props);
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Build the widget iframe URL for a resource URL the widget plays, carrying the parameters of the source it came from.
|
|
16
|
+
*
|
|
17
|
+
* @internal
|
|
18
|
+
*/
|
|
19
|
+
function buildSoundCloudWidgetUrl(resourceUrl, parsed, props = {}) {
|
|
20
|
+
const { referrerPolicy: _referrerPolicy, ...engine } = props.source?.engine?.soundcloud ?? {};
|
|
21
|
+
const params = {
|
|
22
|
+
...DEFAULT_PARAMS,
|
|
23
|
+
...parsed.params,
|
|
24
|
+
...engine
|
|
25
|
+
};
|
|
26
|
+
const query = new URLSearchParams({ url: resourceUrl });
|
|
27
|
+
for (const [key, value] of Object.entries(params)) {
|
|
28
|
+
if (key === "url" || key === "auto_play" || value === null || value === void 0) continue;
|
|
29
|
+
query.set(key, String(value));
|
|
30
|
+
}
|
|
31
|
+
return `${WIDGET_ORIGIN}/player/?${query}`;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Whether the source asks to autoplay through the widget's own `auto_play` parameter.
|
|
35
|
+
*
|
|
36
|
+
* @internal
|
|
37
|
+
*/
|
|
38
|
+
function wantsSoundCloudAutoplay(parsed, source) {
|
|
39
|
+
const value = source?.engine?.soundcloud?.auto_play ?? parsed?.params.auto_play;
|
|
40
|
+
return value === true || value === "true";
|
|
41
|
+
}
|
|
42
|
+
const DEFAULT_PARAMS = {
|
|
43
|
+
visual: false,
|
|
44
|
+
hide_related: true,
|
|
45
|
+
show_comments: false,
|
|
46
|
+
show_reposts: false,
|
|
47
|
+
show_teaser: false,
|
|
48
|
+
buying: false,
|
|
49
|
+
sharing: false,
|
|
50
|
+
download: false
|
|
51
|
+
};
|
|
52
|
+
//#endregion
|
|
53
|
+
export { buildSoundCloudIframeSrc, buildSoundCloudWidgetUrl, parseSoundCloudSource$1 as parseSoundCloudSource, wantsSoundCloudAutoplay };
|
|
54
|
+
|
|
55
|
+
//# sourceMappingURL=source.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"source.js","names":[],"sources":["../../src/source.ts"],"sourcesContent":["import { type ParsedSoundCloudSource, parseSoundCloudSource } from '@videojs/media';\n\nimport type { SoundCloudAdapterProps } from './props';\nimport { WIDGET_ORIGIN } from './widget-api';\n\nexport { type ParsedSoundCloudSource, parseSoundCloudSource, type SoundCloudSourceKind } from '@videojs/media';\n\n/** What a widget parameter can be given as. `null` removes a parameter the embed URL would otherwise carry. */\nexport type SoundCloudWidgetParamValue = string | number | boolean | null | undefined;\n\n/**\n * SoundCloud Widget parameters, spelled as SoundCloud spells them\n * (https://developers.soundcloud.com/docs/api/html5-widget#parameters) and written onto the widget URL. They win over\n * the parameters of a `w.soundcloud.com/player/` URL given as `src`, which win over the host's defaults. Anything not\n * listed still passes through, so parameters SoundCloud adds later keep working.\n *\n * The host's defaults hide the social chrome a player skin has no use for: `buying`, `sharing`, `download`,\n * `show_comments`, `show_reposts`, and `show_teaser` are `false`, `hide_related` is `true`, and `visual` is `false`.\n * The uploader and SoundCloud's own branding stay whenever the widget shows: the widget is what credits the sound.\n */\nexport interface SoundCloudEngineConfig extends Record<string, SoundCloudWidgetParamValue> {\n /** Hex color of the play button and other accents, such as `'#ff5500'`. */\n color?: string | null;\n /** The large artwork layout. Defaults to `false`. */\n visual?: boolean | null;\n /** Start playback once the widget is ready, as `autoplay` does. Never written to the URL. */\n auto_play?: boolean | null;\n /** Index of the track in a set to start from. */\n start_track?: number | null;\n /** When `false`, playing this widget does not pause the other SoundCloud widgets on the page. */\n single_active?: boolean | null;\n /** Show the artwork. */\n show_artwork?: boolean | null;\n /** Show the uploader's name. The widget is what credits the uploader, so keep it unless the page does that elsewhere. */\n show_user?: boolean | null;\n /** Show the play count. */\n show_playcount?: boolean | null;\n /** Show listener comments on the waveform. Defaults to `false`. */\n show_comments?: boolean | null;\n /** Show reposts. Defaults to `false`. */\n show_reposts?: boolean | null;\n /** Show SoundCloud's promotional teaser. Defaults to `false`. */\n show_teaser?: boolean | null;\n /** Hide the related tracks the widget offers once a track ends. Defaults to `true`. */\n hide_related?: boolean | null;\n /** Show the buy button. Defaults to `false`. */\n buying?: boolean | null;\n /** Show the share button. Defaults to `false`. */\n sharing?: boolean | null;\n /** Show the download button. Defaults to `false`. */\n download?: boolean | null;\n /** `referrerpolicy` for the widget iframe. Not a widget parameter. */\n referrerPolicy?: ReferrerPolicy;\n}\n\n/** Structured SoundCloud source: which track or set to play, plus how to play it. */\nexport interface SoundCloudSource {\n /** SoundCloud track, set, or widget embed URL. Mirrors the host's `src` property. */\n src?: string | undefined;\n /** Playback options, keyed by the engine that reads them. */\n engine?: SoundCloudSourceEngineConfig | undefined;\n}\n\n/** The engines a SoundCloud source can configure. */\nexport interface SoundCloudSourceEngineConfig {\n /** SoundCloud Widget parameters, passed through untouched. */\n soundcloud?: SoundCloudEngineConfig | undefined;\n}\n\n/**\n * Build the widget iframe URL for a source. Empty when `src` is not a SoundCloud URL.\n *\n * @internal\n */\nexport function buildSoundCloudIframeSrc(src: string, props: Partial<SoundCloudAdapterProps> = {}): string {\n const parsed = parseSoundCloudSource(src);\n if (!parsed) return '';\n\n return buildSoundCloudWidgetUrl(parsed.url, parsed, props);\n}\n\n/**\n * Build the widget iframe URL for a resource URL the widget plays, carrying the parameters of the source it came from.\n *\n * @internal\n */\nexport function buildSoundCloudWidgetUrl(\n resourceUrl: string,\n parsed: Pick<ParsedSoundCloudSource, 'params'>,\n props: Partial<SoundCloudAdapterProps> = {}\n): string {\n const { referrerPolicy: _referrerPolicy, ...engine } = props.source?.engine?.soundcloud ?? {};\n const params: Record<string, SoundCloudWidgetParamValue> = { ...DEFAULT_PARAMS, ...parsed.params, ...engine };\n const query = new URLSearchParams({ url: resourceUrl });\n\n for (const [key, value] of Object.entries(params)) {\n // The host starts playback itself, so it can apply the volume first.\n if (key === 'url' || key === 'auto_play' || value === null || value === undefined) continue;\n\n query.set(key, String(value));\n }\n\n return `${WIDGET_ORIGIN}/player/?${query}`;\n}\n\n/**\n * Whether the source asks to autoplay through the widget's own `auto_play` parameter.\n *\n * @internal\n */\nexport function wantsSoundCloudAutoplay(\n parsed: Pick<ParsedSoundCloudSource, 'params'> | null,\n source?: SoundCloudSource | null\n) {\n const value = source?.engine?.soundcloud?.auto_play ?? parsed?.params.auto_play;\n\n return value === true || value === 'true';\n}\n\nconst DEFAULT_PARAMS: Record<string, SoundCloudWidgetParamValue> = {\n visual: false,\n hide_related: true,\n show_comments: false,\n show_reposts: false,\n show_teaser: false,\n buying: false,\n sharing: false,\n download: false,\n};\n"],"mappings":";;;;;;;;AA0EA,SAAgB,yBAAyB,KAAa,QAAyC,CAAC,GAAW;CACzG,MAAM,SAAS,sBAAsB,GAAG;CACxC,IAAI,CAAC,QAAQ,OAAO;CAEpB,OAAO,yBAAyB,OAAO,KAAK,QAAQ,KAAK;AAC3D;;;;;;AAOA,SAAgB,yBACd,aACA,QACA,QAAyC,CAAC,GAClC;CACR,MAAM,EAAE,gBAAgB,iBAAiB,GAAG,WAAW,MAAM,QAAQ,QAAQ,cAAc,CAAC;CAC5F,MAAM,SAAqD;EAAE,GAAG;EAAgB,GAAG,OAAO;EAAQ,GAAG;CAAO;CAC5G,MAAM,QAAQ,IAAI,gBAAgB,EAAE,KAAK,YAAY,CAAC;CAEtD,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,MAAM,GAAG;EAEjD,IAAI,QAAQ,SAAS,QAAQ,eAAe,UAAU,QAAQ,UAAU,KAAA,GAAW;EAEnF,MAAM,IAAI,KAAK,OAAO,KAAK,CAAC;CAC9B;CAEA,OAAO,GAAG,cAAc,WAAW;AACrC;;;;;;AAOA,SAAgB,wBACd,QACA,QACA;CACA,MAAM,QAAQ,QAAQ,QAAQ,YAAY,aAAa,QAAQ,OAAO;CAEtE,OAAO,UAAU,QAAQ,UAAU;AACrC;AAEA,MAAM,iBAA6D;CACjE,QAAQ;CACR,cAAc;CACd,eAAe;CACf,cAAc;CACd,aAAa;CACb,QAAQ;CACR,SAAS;CACT,UAAU;AACZ"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { isNumber, isObject, isString } from "@videojs/utils/predicate";
|
|
2
|
+
//#region src/widget-api.ts
|
|
3
|
+
/** Where the widget is served from: commands are posted to it, and only its messages are read. */
|
|
4
|
+
const WIDGET_ORIGIN = "https://w.soundcloud.com";
|
|
5
|
+
/** Read a `message` event's data as a widget message; anything else another frame posts comes back null. */
|
|
6
|
+
function parseSoundCloudMessage(data) {
|
|
7
|
+
if (!isString(data) || !data.includes("\"method\"")) return null;
|
|
8
|
+
let message;
|
|
9
|
+
try {
|
|
10
|
+
message = JSON.parse(data);
|
|
11
|
+
} catch {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
return isObject(message) && isString(message.method) ? message : null;
|
|
15
|
+
}
|
|
16
|
+
function createSoundCloudCommand(method, value) {
|
|
17
|
+
return JSON.stringify(value === void 0 ? { method } : {
|
|
18
|
+
method,
|
|
19
|
+
value
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
function isSoundCloudProgress(value) {
|
|
23
|
+
if (!isObject(value)) return false;
|
|
24
|
+
const { currentPosition } = value;
|
|
25
|
+
return isNumber(currentPosition);
|
|
26
|
+
}
|
|
27
|
+
/** The share of the sound the widget has loaded, under either spelling. */
|
|
28
|
+
function loadedShareOf(progress) {
|
|
29
|
+
const share = progress.loadedProgress ?? progress.loadProgress;
|
|
30
|
+
return isNumber(share) ? share : void 0;
|
|
31
|
+
}
|
|
32
|
+
//#endregion
|
|
33
|
+
export { WIDGET_ORIGIN, createSoundCloudCommand, isSoundCloudProgress, loadedShareOf, parseSoundCloudMessage };
|
|
34
|
+
|
|
35
|
+
//# sourceMappingURL=widget-api.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"widget-api.js","names":[],"sources":["../../src/widget-api.ts"],"sourcesContent":["// Typings and helpers for the postMessage protocol the SoundCloud Widget speaks\n// (https://developers.soundcloud.com/docs/api/html5-widget). The official `api.js` client is a thin wrapper over it,\n// written against `widget.load()`, which rewrites every parameter but `start_track` to `true` or `false` and so cannot\n// carry `color`.\n\nimport { isNumber, isObject, isString } from '@videojs/utils/predicate';\n\n/** Where the widget is served from: commands are posted to it, and only its messages are read. */\nexport const WIDGET_ORIGIN = 'https://w.soundcloud.com';\n\n/** The widget's events, spelled as the protocol spells them. */\nexport type SoundCloudWidgetEvent =\n | 'ready'\n | 'loadProgress'\n | 'playProgress'\n | 'play'\n | 'pause'\n | 'finish'\n | 'seek'\n | 'error'\n | 'sharePanelOpened'\n | 'downloadClicked'\n | 'buyClicked';\n\n/** Getters answer with a message named after themselves, in the order they were asked. */\nexport type SoundCloudWidgetGetter =\n | 'getVolume'\n | 'getDuration'\n | 'getPosition'\n | 'getSounds'\n | 'getCurrentSound'\n | 'getCurrentSoundIndex'\n | 'isPaused';\n\nexport type SoundCloudWidgetCommand =\n | 'play'\n | 'pause'\n | 'toggle'\n | 'seekTo'\n | 'setVolume'\n | 'next'\n | 'prev'\n | 'skip'\n | 'addEventListener'\n | 'removeEventListener';\n\n/**\n * Payload of the audio events. Positions are in milliseconds and `loadedProgress` is the share of the sound loaded, `0`\n * to `1`. The reference documents it as `loadProgress`, so both spellings are read.\n */\nexport interface SoundCloudProgress {\n soundId?: number;\n currentPosition?: number;\n relativePosition?: number;\n loadedProgress?: number;\n loadProgress?: number;\n}\n\n/** A message from the widget. `widgetId` names the document that sent it and changes with every load. */\nexport interface SoundCloudWidgetMessage {\n method: SoundCloudWidgetEvent | SoundCloudWidgetGetter | (string & {});\n value?: unknown;\n widgetId?: string;\n}\n\n/** Read a `message` event's data as a widget message; anything else another frame posts comes back null. */\nexport function parseSoundCloudMessage(data: unknown): SoundCloudWidgetMessage | null {\n if (!isString(data) || !data.includes('\"method\"')) return null;\n\n let message: unknown;\n\n try {\n message = JSON.parse(data);\n } catch {\n return null;\n }\n\n return isObject(message) && isString((message as Partial<SoundCloudWidgetMessage>).method)\n ? (message as SoundCloudWidgetMessage)\n : null;\n}\n\nexport function createSoundCloudCommand(\n method: SoundCloudWidgetCommand | SoundCloudWidgetGetter,\n value?: string | number\n): string {\n return JSON.stringify(value === undefined ? { method } : { method, value });\n}\n\nexport function isSoundCloudProgress(value: unknown): value is SoundCloudProgress {\n if (!isObject(value)) return false;\n\n const { currentPosition } = value as Record<string, unknown>;\n\n return isNumber(currentPosition);\n}\n\n/** The share of the sound the widget has loaded, under either spelling. */\nexport function loadedShareOf(progress: SoundCloudProgress): number | undefined {\n const share = progress.loadedProgress ?? progress.loadProgress;\n\n return isNumber(share) ? share : undefined;\n}\n"],"mappings":";;;AAQA,MAAa,gBAAgB;;AA0D7B,SAAgB,uBAAuB,MAA+C;CACpF,IAAI,CAAC,SAAS,IAAI,KAAK,CAAC,KAAK,SAAS,YAAU,GAAG,OAAO;CAE1D,IAAI;CAEJ,IAAI;EACF,UAAU,KAAK,MAAM,IAAI;CAC3B,QAAQ;EACN,OAAO;CACT;CAEA,OAAO,SAAS,OAAO,KAAK,SAAU,QAA6C,MAAM,IACpF,UACD;AACN;AAEA,SAAgB,wBACd,QACA,OACQ;CACR,OAAO,KAAK,UAAU,UAAU,KAAA,IAAY,EAAE,OAAO,IAAI;EAAE;EAAQ;CAAM,CAAC;AAC5E;AAEA,SAAgB,qBAAqB,OAA6C;CAChF,IAAI,CAAC,SAAS,KAAK,GAAG,OAAO;CAE7B,MAAM,EAAE,oBAAoB;CAE5B,OAAO,SAAS,eAAe;AACjC;;AAGA,SAAgB,cAAc,UAAkD;CAC9E,MAAM,QAAQ,SAAS,kBAAkB,SAAS;CAElD,OAAO,SAAS,KAAK,IAAI,QAAQ,KAAA;AACnC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,51 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@videojs/soundcloud-audio",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "10.3.0",
|
|
4
|
+
"description": "SoundCloud Widget playback adapter for Video.js",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"audio",
|
|
7
|
+
"embed",
|
|
8
|
+
"media",
|
|
9
|
+
"soundcloud",
|
|
10
|
+
"videojs"
|
|
11
|
+
],
|
|
12
|
+
"homepage": "https://videojs.org",
|
|
13
|
+
"license": "Apache-2.0",
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "https://github.com/videojs/video.js",
|
|
17
|
+
"directory": "packages/adapters/soundcloud-audio"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"dist"
|
|
21
|
+
],
|
|
22
|
+
"type": "module",
|
|
23
|
+
"sideEffects": false,
|
|
24
|
+
"main": "dist/default/index.js",
|
|
25
|
+
"module": "dist/default/index.js",
|
|
26
|
+
"types": "dist/dev/index.d.ts",
|
|
27
|
+
"exports": {
|
|
28
|
+
".": {
|
|
29
|
+
"types": "./dist/dev/index.d.ts",
|
|
30
|
+
"development": "./dist/dev/index.js",
|
|
31
|
+
"default": "./dist/default/index.js"
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"publishConfig": {
|
|
35
|
+
"access": "public"
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"@videojs/media": "10.3.0",
|
|
39
|
+
"@videojs/utils": "10.3.0"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"jsdom": "^26.1.0",
|
|
43
|
+
"vite-plus": "1.1.0"
|
|
44
|
+
},
|
|
45
|
+
"scripts": {
|
|
46
|
+
"dev": "vp pack --watch ./src --no-clean",
|
|
47
|
+
"test": "vp test run",
|
|
48
|
+
"test:watch": "vp test",
|
|
49
|
+
"clean": "rimraf --glob dist types '*.tsbuildinfo'"
|
|
50
|
+
}
|
|
6
51
|
}
|