Skip to content

fix: <vimeo-video> element race condition#220

Open
spuppo-mux wants to merge 3 commits intomuxinc:mainfrom
spuppo-mux:fix/vimeo-video-element-race-condition
Open

fix: <vimeo-video> element race condition#220
spuppo-mux wants to merge 3 commits intomuxinc:mainfrom
spuppo-mux:fix/vimeo-video-element-race-condition

Conversation

@spuppo-mux
Copy link
Copy Markdown
Contributor

@spuppo-mux spuppo-mux commented Mar 23, 2026

By checking logs I saw that when the bug happened, we were getting into the isInit === false case (and load was running just one time), so the options weren't being applied.

Tested this mainly on nextjs example. Could originally reproduce semi-consistently by switching to TikTok player and then back to vimeo. When the bug happens we see the Vimeo default color (blueish)
Screenshot 2026-03-20 at 5 30 19 PM
When the config applies correctly we should see a pinkish color
Screenshot 2026-03-20 at 5 29 43 PM

To address the race condition:

  • Moved await (this.#loadRequested = Promise.resolve()); to the top of the function
  • Added a disconnectedCallback to clear state on unmount.

Extra:

  • Code clearness:
    • Moved api listener setup into a new #setupApiListeners function
    • Moved #onLoaded listener to a new method since it's now used from two functions
    • Restrctured "if else" at the bottom of load function so it's clearer to read.
  • Open points (non-blocking):
    • Noticed that isInit and hasLoaded are pretty similar semantically, was wondering if one of them can be removed.
    • Added options to new VimeoPlayerAPI call (did not seem to have effect even though it's specified in the JSdocs)
/**
   * Create a Player.
   *
   * @param {(HTMLIFrameElement|HTMLElement|string|jQuery)} element A reference to the Vimeo
   *        player iframe, and id, or a jQuery object.
   * @param {object} [options] oEmbed parameters to use when creating an embed in the element.
   * @return {Player}
   */

Note

Medium Risk
Changes the load() lifecycle and player initialization timing for the custom element, which can affect autoplay/controls/config application and event ordering. Risk is moderate because it targets an initialization race but touches core state/reset and promise resolution paths.

Overview
Fixes a race in <vimeo-video> initialization by moving the one-tick #loadRequested gating earlier and resetting loadComplete/#hasLoaded after that tick, ensuring options/config are applied consistently on first init vs subsequent loadVideo() calls.

Refactors load completion and listener wiring by extracting shared logic into #onLoaded() and #setupApiListeners(), passes options into new VimeoPlayerAPI(iframe, options), and adds disconnectedCallback() to clear internal init/load flags on unmount to avoid stale state across remounts.

Written by Cursor Bugbot for commit 50ef896. This will update automatically on new commits. Configure here.

@snyk-io
Copy link
Copy Markdown

snyk-io bot commented Mar 23, 2026

Snyk checks have passed. No issues have been found so far.

Status Scan Engine Critical High Medium Low Total (0)
Open Source Security 0 0 0 0 0 issues
Licenses 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

@vercel
Copy link
Copy Markdown

vercel bot commented Mar 23, 2026

@spuppo-mux is attempting to deploy a commit to the Mux Team on Vercel.

A member of the Team first needs to authorize it.

@spuppo-mux
Copy link
Copy Markdown
Contributor Author

Closes #219

@spuppo-mux spuppo-mux linked an issue Mar 23, 2026 that may be closed by this pull request
Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

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

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

<vimeo-video> config not being set

1 participant