DEV Community

Cover image for How to Load the YouTube IFrame Player API Without Breaking Other Plugins
Ryo
Ryo

Posted on • Originally published at karasunouta.com

How to Load the YouTube IFrame Player API Without Breaking Other Plugins

When developing plugins for WordPress, one issue that almost everyone faces at least once is plugin conflicts. The other day, I was developing a plugin (KU Sticky Video for YouTube) that makes YouTube videos in posts follow the scroll to stay in the corner of the screen. It worked perfectly in a clean local testing environment, and I was proud, thinking, “All right, it’s complete!”

However, as soon as I moved the testing ground to a multi-plugin environment where another of my own YouTube-related plugins (unreleased) and other video-related plugins were active, things changed completely. The sticky feature, which had been working smoothly until just moments before, fell completely silent without moving an inch, and when I opened the browser console, there was that annoying red error text. Oh no.

But this is simply a common occurrence in plugin development. In reality, the solution varies depending on the cause of the conflict. In this article, I will explain the mechanism behind the “struggle for leadership” caused by the YouTube Iframe API, which many YouTube-related plugins depend on, in a multi-plugin environment, and introduce a JavaScript design to avoid this problem, share the ride with other scripts, and achieve coexistence.


Why the Conflict? The YouTube API Struggle for Leadership

To highly control YouTube videos from JavaScript, you need to load the official YouTube Iframe Player API. In typical tutorial articles, implementation code like the following is usually introduced:

// Load the API script
const tag = document.createElement('script');
tag.src = 'https://www.youtube.com/iframe_api';
const firstScriptTag = document.getElementsByTagName('script')[0];
firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);

// Define the global function called when loading is complete
window.onYouTubeIframeAPIReady = function() {
    const player = new YT.Player('player-id', {
        events: {
            'onStateChange': onPlayerStateChange
        }
    });
};
Enter fullscreen mode Exit fullscreen mode

While this code seems perfectly fine at first glance, it causes issues in environments where multiple plugins and themes run asynchronously, such as in WordPress. Put simply, what happens if another plugin defines window.onYouTubeIframeAPIReady in exactly the same way? Due to the nature of JavaScript, the initialization function defined first will be completely overwritten and lost by the plugin execution that runs later.

As a result, the plugin that loaded first cannot detect when the API has finished loading, and it will never start. This is exactly why the initial version of KU Sticky Video for YouTube stopped working.


Approaches to a Solution: How to Avoid “Fights” Between Plugins

To prevent this conflict and ensure safe operation in any environment, we decided to take the following countermeasures in the KU Sticky Video for YouTube plugin.

1. “Script Presence Check” to Fill the Gap in Asynchronous Loading

When the loading of the YouTube API script is complete, a window.YT object is automatically created in the browser’s global scope (window). This window.YT object contains all the actual functions of the API for creating and controlling the YouTube player.

The tricky part is that it is not as simple as, “I see. Then I just need to check if window.YT exists.” If you do only that, what happens if the script runs “during the network load (when window.YT is undefined) right after another plugin inserted the tag”? The script tag would be inserted twice. To prevent this, in addition to window.YT, we also check whether a script tag for the YouTube API already exists in the DOM tree.

// Search by partial match, considering URL notation variants (iframe_api / player_api)
const alreadyLoading = document.querySelector(
    'script[src*="youtube.com/iframe_api"], script[src*="youtube.com/player_api"]'
);

if ( ! window.YT && ! alreadyLoading ) {
    // Insert the script tag for the first time here
}
Enter fullscreen mode Exit fullscreen mode

💡 Why continue to check window.YT as well?

At first glance, checking only whether the script tag exists in the DOM (alreadyLoading) seems sufficient. After all, window.YT is supposed to be created after the script is loaded. However, there may be cases where a script tag for the YouTube API does not exist in the DOM—for instance, if another plugin bundles the API directly into its JS via build tools like Vite and loads it.

Therefore, it is necessary to perform a double check using both window.YT to 100% guarantee that the load is already complete, and alreadyLoading to detect via the script tag that “the asynchronous load is in progress right now.” By using both together, the risk of conflicts can be minimized.

2. “Event Sharing (Binding)” via window.YT.get()

If another plugin has already created a YT.Player instance for a YouTube iframe on the page, executing new YT.Player again on top of it will cause an error. Therefore, we use the YouTube API global method window.YT.get() to retrieve the already-created player instance, if it exists, and “share the ride” by binding only our event listeners to it.

let existingPlayer = null;

if ( window.YT && typeof window.YT.get === 'function' ) {
    const id = iframeElement.id;
    // Get by the official spec if an ID exists. Otherwise, fall back to passing the DOM element.
    existingPlayer = ( id ? window.YT.get( id ) : null ) || window.YT.get( iframeElement );
}

if ( existingPlayer ) {
    // Share the event with the existing instance if it exists
    // * Unlike standard DOM, the YouTube API's addEventListener requires the "on" prefix (onStateChange)
    existingPlayer.addEventListener('onStateChange', function(event) {
        handlePlayerStateChange(event);
    });
} else {
    // Otherwise, create a new instance
    const player = new window.YT.Player(iframeElement, { ... });
}
Enter fullscreen mode Exit fullscreen mode

Although the official argument for YT.get() is the “iframe ID string,” we set up a two-step binding process using the unofficial yet highly compatible method of passing the DOM element as a fallback in case there is no ID attribute.

3. “Polling Control” to Prevent Timing Conflicts

The core of these countermeasures is timing control to determine when the API load is complete and when other plugins have finished initialization. For this purpose, we decided to adopt a technique called polling.

However, the YouTube API comes with a global callback function window.onYouTubeIframeAPIReady that is automatically executed by the API when the script loading is complete. Why was such a roundabout method like polling necessary instead of monitoring that standard event handler?

3-1. The Asynchronous Risk Inherent in Wrappers for the Standard “onYouTubeIframeAPIReady”

When considering conflicts with other plugins, a common approach introduced as a standard trick on many development blogs is to “temporarily save the existing callback function into a variable, wrap it, and execute both the other plugin’s initialization and your own in succession” as follows:

// Save the existing callback if there is one
const previousOnReady = window.onYouTubeIframeAPIReady;

// Overwrite the global callback and execute the other processes in succession
window.onYouTubeIframeAPIReady = function () {
    if ( typeof previousOnReady === 'function' ) {
        previousOnReady();
    }
    myPluginInit(); // Initialize our own plugin
};
Enter fullscreen mode Exit fullscreen mode

While this seems like a perfect solution for coexistence at first glance, this implementation actually carries a fatal risk: it cannot handle cases where other plugins initialize the player with asynchronous processing in between.

For example, suppose the other plugin we want to coexist with is designed to initialize by running an asynchronous process, such as “detecting API Ready, fetching configuration data via Ajax, and then creating the player.” In this case, our plugin’s (Sticky’s) initialization process has no way of knowing that the other plugin is waiting for its background asynchronous process to finish. Therefore, at the exact moment of API Ready, it determines that “no conflicting player instance exists yet” and goes ahead to create a new player (new YT.Player) ahead of the other.

By the time the other plugin finishes its asynchronous processing and tries to create its player, our plugin’s initialization is already complete. This causes a double-initialization error for the same iframe element, causing the other plugin to fail. Therefore, if you want to write a robust implementation that accounts for conflicts, you cannot use this onYouTubeIframeAPIReady callback directly as your own initialization trigger.

Our plugin must not interfere at all, such as by overwriting or assigning to the global callback; it should simply load the API, monitor in the background to see “when the other plugin’s initialization is complete,” and then safely share the ride (bind) to it. This is precisely why we need time control using “polling,” which will be explained next.

3-2. The Solution: Controlling Initialization Timing with Polling

To cleanly link the three approaches explained so far—wrapping, double-loading prevention, and event sharing—controlling the timing of the initialization process is extremely important. This is where a design pattern called “polling” comes in handy, which periodically checks the API load status and player initialization status from the program.

In JavaScript, polling is mainly implemented using setInterval. The actual initialization code is as follows:

function initPlayingMode(iframes) {
    // 1. Load the API after checking if another script has already been inserted
    if ( ! window.YT && ! document.querySelector( 'script[src*="youtube.com/iframe_api"], script[src*="youtube.com/player_api"]' ) ) {
        const tag = document.createElement( 'script' );
        tag.src = 'https://www.youtube.com/iframe_api';
        const firstScriptTag = document.getElementsByTagName( 'script' )[ 0 ];
        firstScriptTag.parentNode.insertBefore( tag, firstScriptTag );
    }

    // 2. Poll and monitor the API load and the completion of other plugins' initialization
    let attempts = 0;
    let apiReadyAttempts = 0;
    let setupCompleted = false;
    const checkInterval = setInterval( () => {
        attempts++;
        const isApiReady = window.YT && typeof window.YT.Player === 'function';
        if ( isApiReady ) {
            apiReadyAttempts++;
        }

        // Check if other plugins have finished creating instances for all iframes
        let allFound = false;
        if ( isApiReady && typeof window.YT.get === 'function' ) {
            allFound = true;
            for ( let j = 0; j < iframes.length; j++ ) {
                const id = iframes[ j ].id;
                const player = ( id ? window.YT.get( id ) : null ) || window.YT.get( iframes[ j ] );
                if ( ! player ) {
                    allFound = false;
                    break;
                }
            }
        }

        // Determine the termination conditions
        // - Binding by other plugins is complete (allFound), or
        // - The grace period (300ms) to wait for other plugins' initialization after the API loads has passed (apiReadyAttempts >= 3), or
        // - The maximum timeout (3 seconds) has been reached (attempts >= 30)
        const shouldStop = allFound || apiReadyAttempts >= 3 || attempts >= 30;
        if ( shouldStop ) {
            clearInterval( checkInterval );
            if ( ! setupCompleted ) {
                setupCompleted = true;

                // Perform either "ride-sharing" or "new creation" for each iframe
                setupPlayers( iframes );
            }
        }
    }, 100 );
}
Enter fullscreen mode Exit fullscreen mode

After the API is ready, instead of initializing the player immediately on our own, this code sets up a maximum wait time of 300ms (checking the status up to 3 times every 100ms = polling). If another plugin is attempting to asynchronously create a player at the moment of API Ready, the purpose is to wait a short while for its completion before sharing the ride, thereby preventing double-creation errors as much as possible.

However, if you wait too long and our plugin’s initialization is delayed unnecessarily, there is a risk that the event listeners will not be registered yet by the time the video starts playing. As a result of trial and error balancing this trade-off, this 300ms wait time emerged as the empirical magic number (the best practical threshold) in this development process.

This value might vary depending on the purpose of your plugin.


The Ultimate Question: When Seemingly Perfect Codes Meet

With the implementation so far, we seem to have completed a mostly perfect conflict avoidance code that “does not interfere with the loading of other plugins,” “shares the ride on players created by other plugins,” and “waits 300ms to prevent double creation.” Some readers might also have agreed, thinking, “I see, this implementation looks like it should work fine.”

Here is a question. If Plugin A and Plugin B—two separate plugins, both incorporating the “perfect implementation” explained in this article—are loaded on the same page at the same time, can they coexist?

The answer, unfortunately, is “yes, there are cases, albeit rare, where they will cause conflict errors and go silent (break down).” The cause lies in the “asynchronous registration lag” of the YouTube API. In fact, there is a tiny lag of a few milliseconds between when you execute new YT.Player() and when that player instance is registered inside the API and becomes retrievable via window.YT.get().

If Plugins A and B, which share the same “300ms wait” logic, start their timers at the exact same API Ready timing, both will execute YT.get() almost simultaneously after 300ms. At this moment, since neither has created a player yet, the return value for both will be null. Consequently, both assume that no existing player exists, and both execute new YT.Player() almost at the same time. As a result, a double-creation error occurs on the same iframe, causing both to fail.

You might think, “Then why not detect double creation inside the onReady callback and clean it up with destroy()?” However, the YouTube API’s destroy() forcibly rewrites the DOM elements and clears event handlers, carrying the fatal side effect of taking down and destroying the player that was already running normally. In other words, once a double creation has occurred, it cannot be safely removed afterward.

Introducing “Random Jitter” as the Solution

Since the YouTube API does not provide events for perfect synchronization across multiple plugins, there is no perfect code that can reduce the probability of this timing collision to 0%. However, the solution to suppress this collision probability to practically zero (an ignorable level) is the introduction of “random jitter”.

What we do is very simple. After the 300ms grace period expires, instead of creating the new player immediately, we insert a “random delay of 0 to 100ms” right before execution.

// Executed after the 300ms monitoring termination condition is met
if ( shouldStop ) {
    clearInterval( checkInterval );
    if ( ! setupCompleted ) {
        setupCompleted = true;

        // Disperse execution timing by inserting a random jitter (delay) of 0 to 100ms
        const jitterDelay = Math.random() * 100;
        setTimeout( () => {
            // Check window.YT.get() once more after the random delay expires
            setupPlayers( iframes );
        }, jitterDelay );
    }
}
Enter fullscreen mode Exit fullscreen mode

By adding a random delay, there is a high probability of a discrepancy in the execution timing of Plugin A and Plugin B, which started moving at the same time.

For example, suppose the random delay is 15ms for Plugin A and 78ms for Plugin B. After 15ms, Plugin A, whose delay expired first, creates a new YT.Player. Subsequently, when Plugin B performs its YT.get() check after 78ms, the instance from Plugin A will already be detected. As a result, Plugin B cancels the creation of a new instance and safely shares the ride on Plugin A.

The beauty of this design is that “if all plugins depending on the YouTube API adopt this ‘concession via random jitter’ design, conflict-free coexistence (ride-sharing) can be achieved autonomously in almost all cases.” This is, so to speak, a piece of wisdom to achieve coexistence by writing the spirit of mutual concession into code without waiting for improvements on the API side.

We have also built this collision avoidance and coexistence design using random jitter into the sticky plugin we developed, “KU Sticky Video for YouTube” (and its paid Pro version).


Conclusion

Using the API of a major external service like YouTube in a plugin inevitably means harboring the potential for conflicts.

Users embed YouTube videos in their posts and apply various customizations to them. For this, it is naturally expected that they will use multiple YouTube-related plugins together. In anticipation of this, designing an implementation that allows the dependent API to be used without conflicts is indispensable.

The recently developed KU Sticky Video for YouTube and its paid version, KU Sticky Video for YouTube Pro, were also successfully released after identifying and resolving these issues one by one during development. If you want to showcase YouTube videos effectively alongside text, we highly recommend giving them a try.

Please also try using them alongside other YouTube plugins! They should work without any problems (but if you do run into issues, please let me know quietly via the contact form).

Top comments (0)