YouTube iframe API: how do I control an iframe player that's already in the HTML?

JavascriptYoutube ApiYoutube Javascript-Api

Javascript Problem Overview


I want to be able to control iframe based YouTube players. This players will be already in the HTML, but I want to control them via the JavaScript API.

I've been reading the documentation for the iframe API which explain how to add a new video to the page with the API, and then control it with the YouTube player functions:

var player;
function onYouTubePlayerAPIReady() {
    player = new YT.Player('container', {
        height: '390',
        width: '640',
        videoId: 'u1zgFlCw8Aw',
        events: {
            'onReady': onPlayerReady,
            'onStateChange': onPlayerStateChange
        }
    });
}

That code creates a new player object and assigns it to 'player', then inserts it inside the #container div. Then I can operate on 'player' and call playVideo(), pauseVideo(), etc. on it.

But I want to be able to operate on iframe players which are already on the page.

I could do this very easily with the old embed method, with something like:

player = getElementById('whateverID');
player.playVideo();

But this doesn't work with the new iframes. How can I assign a iframe object already on the page and then use the API functions on it?

Javascript Solutions


Solution 1 - Javascript

Fiddle Links: Source code - Preview - [Small version](http://jsfiddle.net/8R5y6/ "Note: The small version does NOT take care of the player's state!")
Update: This small function will only execute code in a single direction. If you want full support (eg event listeners / getters), have a look at [Listening for Youtube Event in jQuery][1]

As a result of a deep code analysis, I've created a function: function callPlayer requests a function call on any framed YouTube video. See the YouTube Api reference to get a full list of possible function calls. Read the comments at the source code for an explanation.

On 17 may 2012, the code size was doubled in order to take care of the player's ready state. If you need a compact function which does not deal with the player's ready state, see http://jsfiddle.net/8R5y6/.

/**
 * @author       Rob W <gwnRob@gmail.com>
 * @website      https://stackoverflow.com/a/7513356/938089
 * @version      20190409
 * @description  Executes function on a framed YouTube video (see website link)
 *               For a full list of possible functions, see:
 *               https://developers.google.com/youtube/js_api_reference
 * @param String frame_id The id of (the div containing) the frame
 * @param String func     Desired function to call, eg. "playVideo"
 *        (Function)      Function to call when the player is ready.
 * @param Array  args     (optional) List of arguments to pass to function func*/
function callPlayer(frame_id, func, args) {
    if (window.jQuery && frame_id instanceof jQuery) frame_id = frame_id.get(0).id;
    var iframe = document.getElementById(frame_id);
    if (iframe && iframe.tagName.toUpperCase() != 'IFRAME') {
        iframe = iframe.getElementsByTagName('iframe')[0];
    }

    // When the player is not ready yet, add the event to a queue
    // Each frame_id is associated with an own queue.
    // Each queue has three possible states:
    //  undefined = uninitialised / array = queue / .ready=true = ready
    if (!callPlayer.queue) callPlayer.queue = {};
    var queue = callPlayer.queue[frame_id],
        domReady = document.readyState == 'complete';

    if (domReady && !iframe) {
        // DOM is ready and iframe does not exist. Log a message
        window.console && console.log('callPlayer: Frame not found; id=' + frame_id);
        if (queue) clearInterval(queue.poller);
    } else if (func === 'listening') {
        // Sending the "listener" message to the frame, to request status updates
        if (iframe && iframe.contentWindow) {
            func = '{"event":"listening","id":' + JSON.stringify(''+frame_id) + '}';
            iframe.contentWindow.postMessage(func, '*');
        }
    } else if ((!queue || !queue.ready) && (
               !domReady ||
               iframe && !iframe.contentWindow ||
               typeof func === 'function')) {
        if (!queue) queue = callPlayer.queue[frame_id] = [];
        queue.push([func, args]);
        if (!('poller' in queue)) {
            // keep polling until the document and frame is ready
            queue.poller = setInterval(function() {
                callPlayer(frame_id, 'listening');
            }, 250);
            // Add a global "message" event listener, to catch status updates:
            messageEvent(1, function runOnceReady(e) {
                if (!iframe) {
                    iframe = document.getElementById(frame_id);
                    if (!iframe) return;
                    if (iframe.tagName.toUpperCase() != 'IFRAME') {
                        iframe = iframe.getElementsByTagName('iframe')[0];
                        if (!iframe) return;
                    }
                }
                if (e.source === iframe.contentWindow) {
                    // Assume that the player is ready if we receive a
                    // message from the iframe
                    clearInterval(queue.poller);
                    queue.ready = true;
                    messageEvent(0, runOnceReady);
                    // .. and release the queue:
                    while (tmp = queue.shift()) {
                        callPlayer(frame_id, tmp[0], tmp[1]);
                    }
                }
            }, false);
        }
    } else if (iframe && iframe.contentWindow) {
        // When a function is supplied, just call it (like "onYouTubePlayerReady")
        if (func.call) return func();
        // Frame exists, send message
        iframe.contentWindow.postMessage(JSON.stringify({
            "event": "command",
            "func": func,
            "args": args || [],
            "id": frame_id
        }), "*");
    }
    /* IE8 does not support addEventListener... */
    function messageEvent(add, listener) {
        var w3 = add ? window.addEventListener : window.removeEventListener;
        w3 ?
            w3('message', listener, !1)
        :
            (add ? window.attachEvent : window.detachEvent)('onmessage', listener);
    }
}

Usage:

callPlayer("whateverID", function() {
    // This function runs once the player is ready ("onYouTubePlayerReady")
    callPlayer("whateverID", "playVideo");
});
// When the player is not ready yet, the function will be queued.
// When the iframe cannot be found, a message is logged in the console.
callPlayer("whateverID", "playVideo");

##Possible questions (& answers): Q: It doesn't work!
A: "Doesn't work" is not a clear description. Do you get any error messages? Please show the relevant code.

Q: playVideo does not play the video.
A: Playback requires user interaction, and the presence of allow="autoplay" on the iframe. See https://developers.google.com/web/updates/2017/09/autoplay-policy-changes and https://developer.mozilla.org/en-US/docs/Web/Media/Autoplay_guide

Q: I have embedded a YouTube video using <iframe src="http://www.youtube.com/embed/As2rZGPGKDY" />but the function doesn't execute any function!
A: You have to add ?enablejsapi=1 at the end of your URL: /embed/vid_id?enablejsapi=1.

Q: I get error message "An invalid or illegal string was specified". Why?
A: The API doesn't function properly at a local host (file://). Host your (test) page online, or use JSFiddle. Examples: See the links at the top of this answer.

Q: How did you know this?
A: I have spent some time to manually interpret the API's source. I concluded that I had to use the postMessage method. To know which arguments to pass, I created a Chrome extension which intercepts messages. The source code for the extension can be downloaded [here][3].

Q: What browsers are supported?
A: Every browser which supports JSON and postMessage.

  • IE 8+
  • Firefox 3.6+ (actually 3.5, but document.readyState was implemented in 3.6)
  • Opera 10.50+
  • Safari 4+
  • Chrome 3+

Related answer / implementation: Fade-in a framed video using jQuery
Full API support: [Listening for Youtube Event in jQuery][4]
Official API: https://developers.google.com/youtube/iframe_api_reference

###Revision history

Solution 2 - Javascript

Looks like YouTube has updated their JS API so this is available by default! You can use an existing YouTube iframe's ID...

<iframe id="player" src="http://www.youtube.com/embed/M7lc1UVf-VE?enablejsapi=1&origin=http://example.com" frameborder="0"></iframe>

...in your JS...

var player;
function onYouTubeIframeAPIReady() {
  player = new YT.Player('player', {
    events: {
      'onStateChange': onPlayerStateChange
    }
  });
}

function onPlayerStateChange() {
  //...
}

...and the constructor will use your existing iframe instead of replacing it with a new one. This also means you don't have to specify the videoId to the constructor.

See Loading a video player

Solution 3 - Javascript

You can do this with far less code:

function callPlayer(func, args) {
    var i = 0,
        iframes = document.getElementsByTagName('iframe'),
        src = '';
    for (i = 0; i < iframes.length; i += 1) {
        src = iframes[i].getAttribute('src');
        if (src && src.indexOf('youtube.com/embed') !== -1) {
            iframes[i].contentWindow.postMessage(JSON.stringify({
                'event': 'command',
                'func': func,
                'args': args || []
            }), '*');
        }
    }
}

Working example: http://jsfiddle.net/kmturley/g6P5H/296/

Solution 4 - Javascript

My own version of Kim T's code above which combines with some jQuery and allows for targeting of specific iframes.

$(function() {
	callPlayer($('#iframe')[0], 'unMute');
});

function callPlayer(iframe, func, args) {
	if ( iframe.src.indexOf('youtube.com/embed') !== -1) {
		iframe.contentWindow.postMessage( JSON.stringify({
			'event': 'command',
			'func': func,
			'args': args || []
		} ), '*');
	}
}

Solution 5 - Javascript

I was having issues with the above examples so instead, I just inserted the iframe on click in with JS with autoplay in the source and it works fine for me. I also had the possibility for Vimeo or YouTube so I needed to be able to handle that.

This solution isn't amazing and could be cleaned up but this worked for me. I also don't like jQuery but the project was already using it and I was just refactoring existing code, feel free to clean up or convert to vanilla JS :)

<!-- HTML -->
<div class="iframe" data-player="viemo" data-src="$PageComponentVideo.VideoId"></div>


<!-- jQuery -->
$(".btnVideoPlay").on("click", function (e) {
        var iframe = $(this).parents(".video-play").siblings(".iframe");
        iframe.show();

        if (iframe.data("player") === "youtube") {
            autoPlayVideo(iframe, iframe.data("src"), "100%", "100%");
        } else {
            autoPlayVideo(iframe, iframe.data("src"), "100%", "100%", true);
        }
    });

    function autoPlayVideo(iframe, vcode, width, height, isVimeo) {
        if (isVimeo) {
            iframe.html(
                '<iframe width="' +
                    width +
                    '" height="' +
                    height +
                    '" src="https://player.vimeo.com/video/' +
                    vcode +
                    '?color=ff9933&portrait=0&autoplay=1" frameborder="0" allowfullscreen wmode="Opaque"></iframe>'
            );
        } else {
            iframe.html(
                '<iframe width="' +
                    width +
                    '" height="' +
                    height +
                    '" src="https://www.youtube.com/embed/' +
                    vcode +
                    '?autoplay=1&loop=1&rel=0&wmode=transparent" frameborder="0" allowfullscreen wmode="Opaque"></iframe>'
            );
        }
    }

Solution 6 - Javascript

Thank you Rob W for your answer.

I have been using this within a Cordova application to avoid having to load the API and so that I can easily control iframes which are loaded dynamically.

I always wanted the ability to be able to extract information from the iframe, such as the state (getPlayerState) and the time (getCurrentTime).

Rob W helped highlight how the API works using postMessage, but of course this only sends information in one direction, from our web page into the iframe. Accessing the getters requires us to listen for messages posted back to us from the iframe.

It took me some time to figure out how to tweak Rob W's answer to activate and listen to the messages returned by the iframe. I basically searched through the source code within the YouTube iframe until I found the code responsible for sending and receiving messages.

The key was changing the 'event' to 'listening', this basically gave access to all the methods which were designed to return values.

Below is my solution, please note that I have switched to 'listening' only when getters are requested, you can tweak the condition to include extra methods.

Note further that you can view all messages sent from the iframe by adding a console.log(e) to the window.onmessage. You will notice that once listening is activated you will receive constant updates which include the current time of the video. Calling getters such as getPlayerState will activate these constant updates but will only send a message involving the video state when the state has changed.

function callPlayer(iframe, func, args) {
    iframe=document.getElementById(iframe);
    var event = "command";
    if(func.indexOf('get')>-1){
	    event = "listening";
    }

    if ( iframe&&iframe.src.indexOf('youtube.com/embed') !== -1) {
      iframe.contentWindow.postMessage( JSON.stringify({
          'event': event,
          'func': func,
          'args': args || []
      }), '*');
    }
}
window.onmessage = function(e){
    var data = JSON.parse(e.data);
    data = data.info;
    if(data.currentTime){
	    console.log("The current time is "+data.currentTime);
    }
    if(data.playerState){
	    console.log("The player state is "+data.playerState);
    }
}

Solution 7 - Javascript

One quick solution, if requests are not an issue, and you're wanting this behavior for something like a show/hide video, is to remove/add the iframe, or cleaning and filling the src.

const stopPlayerHack = (iframe) => {
	let src = iframe.getAttribute('src');
	iframe.setAttribute('src', '');
	iframe.setAttribute('src', src);
}

The iframe will be removed, stop to play and will be loaded right after that. In my case I've improved the code to only set the src again on lightbox open, so the load will only happen if user demands to see the video.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
Questionagente_secretoView Question on Stackoverflow
Solution 1 - JavascriptRob WView Answer on Stackoverflow
Solution 2 - JavascriptCletusWView Answer on Stackoverflow
Solution 3 - JavascriptKim TView Answer on Stackoverflow
Solution 4 - JavascriptadamjView Answer on Stackoverflow
Solution 5 - JavascriptBrady EdgarView Answer on Stackoverflow
Solution 6 - JavascriptDanbardoView Answer on Stackoverflow
Solution 7 - JavascriptAllysonSouzaView Answer on Stackoverflow