Skip to content
110 changes: 75 additions & 35 deletions packages/vimeo-video-element/vimeo-video-element.js
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,10 @@ class VimeoVideoElement extends MediaPlayedRangesMixin(globalThis.HTMLElement ??
#videoWidth = NaN;
#videoHeight = NaN;
#config = null;
/** Distinguishes a remount from SSR hydration.
* See load()
*/
#wasDisconnected = false;

constructor() {
super();
Expand Down Expand Up @@ -136,17 +140,13 @@ class VimeoVideoElement extends MediaPlayedRangesMixin(globalThis.HTMLElement ??
}

set config(value) {
if (JSON.stringify(this.#config) === JSON.stringify(value)) return;
this.#config = value;
this.load();
Comment thread
cursor[bot] marked this conversation as resolved.
}

async load() {
if (this.#loadRequested) return;

const isFirstLoad = !this.#hasLoaded;
Comment thread
cursor[bot] marked this conversation as resolved.

if (this.#hasLoaded) this.loadComplete = new PublicPromise();
this.#hasLoaded = true;

// Wait 1 tick to allow other attributes to be set.
await (this.#loadRequested = Promise.resolve());
this.#loadRequested = null;
Expand All @@ -169,9 +169,15 @@ class VimeoVideoElement extends MediaPlayedRangesMixin(globalThis.HTMLElement ??
this.api = null;

if (!this.src) {
// Nothing to load. Leave loadComplete and #hasLoaded untouched so
// callers awaiting the existing loadComplete aren't orphaned if a
// later load() (e.g. triggered by a subsequent src) replaces it.
return;
}

if (this.#hasLoaded) this.loadComplete = new PublicPromise();
this.#hasLoaded = true;

this.dispatchEvent(new Event('loadstart'));

// https://developer.vimeo.com/player/sdk/embed
Expand All @@ -187,50 +193,86 @@ class VimeoVideoElement extends MediaPlayedRangesMixin(globalThis.HTMLElement ??
...this.#config,
};

const onLoaded = async () => {
this.#readyState = 1; // HTMLMediaElement.HAVE_METADATA
this.dispatchEvent(new Event('loadedmetadata'));

if (this.api) {
this.#muted = await this.api.getMuted();
this.#volume = await this.api.getVolume();
this.dispatchEvent(new Event('volumechange'));

this.#duration = await this.api.getDuration();
this.dispatchEvent(new Event('durationchange'));
}

this.dispatchEvent(new Event('loadcomplete'));
this.loadComplete.resolve();
};

if (this.#isInit) {
this.api = oldApi;
await this.api.loadVideo({
...options,
url: this.src,
});
await onLoaded();
await this.#onLoaded();
await this.loadComplete;
return;
}

this.#isInit = true;

let iframe = this.shadowRoot?.querySelector('iframe');
/*
* Decide whether to build the iframe or adopt an existing one:
* - First client mount or remount after disconnect: build, so the iframe
* URL reflects current src and config. The Vimeo SDK wraps an existing
* iframe as-is and won't update its URL.
* - SSR declarative shadow DOM hydration: adopt the existing iframe. Its
* URL is already correct, and rebuilding would destroy the in-flight
* request and cause a visible stutter. Recover config from the
* data-config attribute so element state matches the DOM.
*/
if (!this.shadowRoot) this.attachShadow({ mode: 'open' });

const existingIframe = this.shadowRoot.querySelector('iframe');
const isSsrHydration = existingIframe && !this.#wasDisconnected;

if (isSsrHydration) {
if (!this.#config) {
this.#config = JSON.parse(existingIframe.getAttribute('data-config') || '{}');
}
} else {
this.shadowRoot.innerHTML = getTemplateHTML(namedNodeMapToObject(this.attributes), this);
}
this.#wasDisconnected = false;

if (isFirstLoad && iframe) {
this.#config = JSON.parse(iframe.getAttribute('data-config') || '{}');
oldApi?.destroy?.();
const iframe = this.shadowRoot.querySelector('iframe');
this.api = new VimeoPlayerAPI(iframe);
this.#setupApiListeners();
await this.loadComplete;
}

connectedCallback() {
if (this.#wasDisconnected) {
this.load();
}
super.connectedCallback?.();
}

if (!this.shadowRoot) {
this.attachShadow({ mode: 'open' });
this.shadowRoot.innerHTML = getTemplateHTML(namedNodeMapToObject(this.attributes), this);
iframe = this.shadowRoot.querySelector('iframe');
disconnectedCallback() {
this.#wasDisconnected = true;
this.#loadRequested = null;
this.#hasLoaded = null;
this.#isInit = null;
this.loadComplete = new PublicPromise();
this.api?.destroy?.();
this.api = null;
super.disconnectedCallback?.();
Comment thread
cursor[bot] marked this conversation as resolved.
}
Comment thread
cursor[bot] marked this conversation as resolved.

#onLoaded = async () => {
this.#readyState = 1; // HTMLMediaElement.HAVE_METADATA
this.dispatchEvent(new Event('loadedmetadata'));

if (this.api) {
this.#muted = await this.api.getMuted();
this.#volume = await this.api.getVolume();
this.dispatchEvent(new Event('volumechange'));

this.#duration = await this.api.getDuration();
this.dispatchEvent(new Event('durationchange'));
}

this.api = new VimeoPlayerAPI(iframe);
this.dispatchEvent(new Event('loadcomplete'));
this.loadComplete.resolve();
};

#setupApiListeners() {
const textTracksVideo = document.createElement('video');
this.textTracks = textTracksVideo.textTracks;
this.api.getTextTracks().then((vimeoTracks) => {
Expand All @@ -249,7 +291,7 @@ class VimeoVideoElement extends MediaPlayedRangesMixin(globalThis.HTMLElement ??

const onceLoaded = () => {
this.api.off('loaded', onceLoaded);
onLoaded();
this.#onLoaded();
};
this.api.on('loaded', onceLoaded);

Expand Down Expand Up @@ -333,8 +375,6 @@ class VimeoVideoElement extends MediaPlayedRangesMixin(globalThis.HTMLElement ??
this.#videoHeight = videoHeight;
this.dispatchEvent(new Event('resize'));
});

await this.loadComplete;
}

async attributeChangedCallback(attrName, oldValue, newValue) {
Expand Down
Loading