Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 44 additions & 36 deletions packages/vimeo-video-element/vimeo-video-element.js
Original file line number Diff line number Diff line change
Expand Up @@ -138,17 +138,16 @@ class VimeoVideoElement extends MediaPlayedRangesMixin(globalThis.HTMLElement ??
}

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

const isFirstLoad = !this.#hasLoaded;

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

// Wait 1 tick to allow other attributes to be set.
await (this.#loadRequested = Promise.resolve());
this.#loadRequested = null;

if (this.#hasLoaded) this.loadComplete = new PublicPromise();
this.#hasLoaded = true; // TODO: Identify how hasLoaded differs from isInit
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reordered loadComplete refresh enables null api dereference

Medium Severity

Moving if (this.#hasLoaded) this.loadComplete = new PublicPromise() from before the 1-tick await to after it introduces a timing regression on second+ loads. When src and loop change in the same synchronous block (common in React/Next.js), the loop handler's await this.loadComplete resolves immediately on the stale promise. Its continuation then runs after load() sets this.api = null, causing this.api.setLoop(...) to throw a TypeError. Previously, loadComplete was refreshed before the await, so the loop handler would correctly suspend until the new load completed.

Additional Locations (1)
Fix in Cursor Fix in Web


this.#currentTime = 0;
this.#duration = NaN;
this.#muted = false;
Expand Down Expand Up @@ -185,50 +184,61 @@ 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;
}
} else {
this.#isInit = true;

this.#isInit = true;
let iframe = this.shadowRoot?.querySelector('iframe');

let iframe = this.shadowRoot?.querySelector('iframe');
if (isFirstLoad && iframe) {
this.#config = JSON.parse(iframe.getAttribute('data-config') || '{}');
}

if (!this.shadowRoot) {
this.attachShadow({ mode: 'open' });
this.shadowRoot.innerHTML = getTemplateHTML(namedNodeMapToObject(this.attributes), this);
iframe = this.shadowRoot.querySelector('iframe');
}

if (isFirstLoad && iframe) {
this.#config = JSON.parse(iframe.getAttribute('data-config') || '{}');
this.api = new VimeoPlayerAPI(iframe, options);
this.#setupApiListeners();
await this.loadComplete;
}
}

disconnectedCallback() {
this.#loadRequested = null;
this.#hasLoaded = null;
this.#isInit = null;
super.disconnectedCallback?.()
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale loadComplete after disconnect causes premature resolution

High Severity

disconnectedCallback resets #hasLoaded to null but doesn't reset loadComplete. After reconnect, load() checks if (this.#hasLoaded) which is null (falsy), so it skips creating a new PublicPromise. The old already-resolved loadComplete is reused, causing await this.loadComplete at line 213 to resolve immediately — before the video actually loads. This also affects play(), pause(), and other methods that await this.loadComplete. Adding this.loadComplete = new PublicPromise() to disconnectedCallback would fix this.

Additional Locations (1)
Fix in Cursor Fix in Web


if (!this.shadowRoot) {
this.attachShadow({ mode: 'open' });
this.shadowRoot.innerHTML = getTemplateHTML(namedNodeMapToObject(this.attributes), this);
iframe = this.shadowRoot.querySelector('iframe');
#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 @@ -247,7 +257,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 @@ -331,8 +341,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