I'm attempting to use the YouTube API to control a set of players in a slideshow. I want to be able to stop and play videos depending on which frame the slideshow is on. So I'm pushing the players into an array based on the id of the frame. When a fame changes I call stop on the current one and start on the new one. However even in the most basic case I'm getting this error:
Uncaught TypeError: Object #<S> has no method 'playVideo'
here is the code for the simple test
<!-- WIDGET YOUTUBE VIDEO -->
<div class="widget_youtube_video" id="wyv_1-5">
<iframe id="ytplayer_1-5" width="560" height="315" src="//www.youtube.com/embed/CxW8TLtUMfM?autoplay=0&enablejsapi=1&origin=http://mmgi.localhost/" frameborder="0" allowfullscreen></iframe>
<script type="text/javascript">
var tag = document.createElement('script');
tag.src = "//www.youtube.com/iframe_api";
var firstScriptTag = document.getElementsByTagName('script')[0];
firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
var yt_players = {};
function onYouTubeIframeAPIReady() {
yt_players['1-5'] = new YT.Player('ytplayer_1-5');
yt_players['1-5'].playVideo();
}
</script>
</div><!-- end widget_youtube_video -->
I've already tried removing the origin tag from the url to check if that was causing the issues, but it still gave me the same error. Any help would be greatly appreciated as I have no idea where to go from here.
Thanks in advance.
EDIT:
I also tried to put the player in a non-arrayed object and it also did not work.
A couple of things I see going on that may be of use. First of all, removing the origin parameter will help during development, as it prevents access to the API in general if A) it doesn't match exactly, and B) sometimes for no reason when on localhost.
As you note, though, even when removing it in your case the API isn't responding. This is because creating a YT.player object consumes a bit of time, and so you then are trying to trigger a playVideo method before the object is fully initialized. Instead you should utilize the onReady callback parameter of the YT.Player object, like this:
var tag = document.createElement('script');
tag.src = "//www.youtube.com/iframe_api";
var firstScriptTag = document.getElementsByTagName('script')[0];
firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
var yt_players = {};
function onYouTubeIframeAPIReady() {
yt_players['1-5'] = new YT.Player('ytplayer_1-5', {
events: {'onReady': onPlayerReady} // bind to callback for when object is ready
});
}
function onPlayerReady(event) {
event.target.playVideo(); // this is kept generic so the same callback can be used with any player object
}
Here's a fiddle with the working code:
http://jsfiddle.net/jlmcdonald/dEjXL/
Related
I'm building an online 'TV' which will use YouTube live-streams for multiple channels.
The channels are contained within tabs. The videos need to be stopped when changing tabs otherwise you can hear the audio in the background.
Here's a link to the JSFiddle: https://jsfiddle.net/matlow/08k4csuh/
I've managed to turn the 'Channel 1' off when changing to another channel with:
var iframe = document.getElementsByClassName("tvscreen")[0].contentWindow;
and
iframe.postMessage('{"event":"command","func":"pauseVideo","args":""}', '*');
In the tab javascript for loop which also handles the tabcontent[i].style.display = "none";
I think I need to use the for loop to call each instance of the iframe... but I'm quite new to javascript so I'm not quite sure how to achieve this.
It will also help to use iframe.postMessage('{"event":"command","func":"playVideo","args":""}', '*'); so the video plays automatically again when clicking on the relevant tab... but again I'm not quite sure how to implement this.
I've been working on this for a few days so if anyone had any tips or pointers I would really appreciate it!
Thanks for reading! :)
You are not using YouTube's API properly. See https://developers.google.com/youtube/iframe_api_reference
In your fiddle, programmatic play is not possible, because you can't know when the YouTube player is ready, as you are not the one initialising it. Your attempts to play the video might take place too early.
Programmatic pause (you managed to pause the first video) is possible thanks to enablejsapi=1 in the iframe src and the fact that the player is ready at that point.
Here's a fork of your fiddle - https://jsfiddle.net/raven0us/ancr2fgz
I added a couple of comments. Check those out.
// load YouTube iframe API as soon as possible, taken from their docs
var tag = document.createElement('script');
tag.id = 'iframe-demo';
tag.src = 'https://www.youtube.com/iframe_api';
var firstScriptTag = document.getElementsByTagName('script')[0];
firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
// initialised players are kept here so we don't have to destroy and reinit
var ytPlayers = {};
function mountChannel(channel) {
var player;
var iframeContainer = document.querySelectorAll('#' + channel + ' iframe');
// if the channel & iframe we want to "mount" exist, check for playing iframes before doing anything else
if (iframeContainer.length > 0) {
// Object.keys() is ECMA 5+, sorry about this, but no easy to check if an object is empty
// alternatively, you could have an array, but in that case, you won't be able to fetch a specific player as fast
// if you don't need that functionality, array is as good cause you will just loop through active players and destroy them
var activePlayersKeys = Object.keys(ytPlayers);
if (activePlayersKeys.length > 0) { // if players exist in the pool, destroy them
for (var i = 0; i < activePlayersKeys.length; i++) {
var activeChannel = activePlayersKeys[i];
var activePlayer = ytPlayers[activeChannel];
activePlayer.getIframe().classList.remove('playing'); // mark pause accordingly, by removing class, not necessary
activePlayer.pauseVideo();
}
}
// check if player already initialised and if player exists, check if it has resumeVideo as a function
if (ytPlayers.hasOwnProperty(channel)) {
ytPlayers[channel].playVideo();
} else {
var iframe = iframeContainer[0];
player = new YT.Player(iframe, {
events: {
'onReady': function (event) {
// event.target is the YT player
// get the actual DOM node iframe nad mark it as playing via a class, styling purposes, not necessary
event.target.getIframe().classList.add('playing');
// play the video
event.target.playVideo();
// video may not autoplay all the time in Chrome, despite its state being cued and this event getting triggered, this happens due to a lot of factors
},
// you should also implement `onStateChange` in order to track video state (as a result of user actions directly via YouTube controls) - https://developers.google.com/youtube/iframe_api_reference#Events
}
});
// append to the list
ytPlayers[channel] = player;
}
}
}
// Get the element with id="defaultOpen" and click on it
function onYouTubeIframeAPIReady() {
// YouTube API will call this when it's ready, only then attempt to "mount" the initial channel
document.getElementById("defaultOpen").click();
}
This is the first time I worked with YouTube's iframe API, but it seems reasonable.
I have embedded iFrame Youtube videos onto my page and I want the video to pause when the user clicks on the 'next' button to cycle through the carousels of videos. Before I cycle to the next video, I create a YT Object for the active video and try to run the pauseVideo() function. The console reports the error: Uncaught TypeError: Object #<S> has no method 'pauseVideo'.
What is puzzling is that I have console.log the player before calling the pauseVideo() function and I do see the pauseVideo() method available in the object. Does anyone have any idea why I am able to see the method in the YT Object, but the console keeps reporting that the method does not exist?
JS-Fiddle: http://jsfiddle.net/7WPe9/
// Load the IFrame Player API code asynchronously on DOM load
var tag = document.createElement('script');
tag.src = "https://www.youtube.com/player_api";
var firstScriptTag = document.getElementsByTagName('script')[0];
firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
// Triggered when user clicks 'next slide'
pauseActiveYoutubeVideos : function() {
var player = new YT.Player( $('#media .item.active iframe.ytplayer')[0] );
console.log( player ); //pauseVideo() is defined in this player object
player.pauseVideo(); //console reports 'Uncaught TypeError: Object #<S> has no method 'pauseVideo''
}
Lots of little things here adding up to your problem. First of all, you have to send in an ID as an argument rather than a jquery object (as pointed out in one of the comments). More importantly, the YT iframe API has trouble when you bind an iFrame to a player object and then remove it ... you can't then rebind to that same iFrame again. So your nested loops need to be replaced. One strategy that generally works is to set up a separate tracking object where you create a YT.Player object for each video right at the beginning, and then just keep track of which one should be the active one (so you know which one to pause). Try something like this (and it should just be in a <script> tag, rather than in a jQuery 'ready' function, so as to avoid problems with the iFrame API's asynchronous load):
// Load the IFrame Player API code asynchronously on DOM load
var tag = document.createElement('script');
tag.src = "http://www.youtube.com/player_api";
var firstScriptTag = document.getElementsByTagName('script')[0];
firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
// Sets up player tracker, and init the carousel
var players={}, activePlayerId;
function onYouTubeIframeAPIReady() {
$('iframe.ytplayer').each(function() {
players[$(this).attr('id')]=new YT.Player($(this).attr('id'));
});
activePlayerId=$('iframe.ytplayer').first().attr('id');
$("#"+activePlayerId).addClass('active');
// Click on carousel next button
$('#next').on('click', function() {
players[activePlayerId].pauseVideo();
$("#"+activePlayerId).removeClass('active');
if ($("#"+activePlayerId).next().is('iframe')) {
activePlayerId=$("#"+activePlayerId).next().attr('id');
}
else {
activePlayerId=$('iframe.ytplayer').first().attr('id');
}
$("#"+activePlayerId).addClass('active');
});
}
When you create player object, make sure that 'onReady' event happened before you call 'pauseVideo' method.
For example, you can stop the video this way:
var player = new window.YT.Player(yourIframeElement, {
events: {
'onReady': function(event) {
event.target.pauseVideo();
}
}
});
Notice: if you try to create the object with the same video element again, 'onReady' event won't happen anymore.
I've scoured the docs in SO and Vimeo and can't seem to figure out how to call an API method outside of the Ready Event in Vimeo. I have created my Vimeo player, embedding it into teacher.js:
$(".video-player").prepend('<iframe id="player" src="//player.vimeo.com/video/'+video_id+'?api=1&player_id=player&badge=0&title=0&byline=0&portrait=0" width="100%" height="100%" frameborder="0" webkitallowfullscreen mozallowfullscreen allowfullscreen></iframe>');
player = $('iframe#player')[0];
vimeoSetup(player);
Which then calls vimeoSetup in a different script:
function vimeoSetup (player) {
$f(player).addEvent('ready', ready);
function addEvent(element, eventName, callback) {
if (element.addEventListener) {
element.addEventListener(eventName, callback, false);
}
else {
element.attachEvent('on'+eventName, callback);
}
}
function ready(player_id) {
// Keep a reference to Froogaloop for this player
container = document.getElementById(player_id).parentNode.parentNode;
froogaloop = $f(player_id);
var buttons = container.querySelector('div#video-player-controls-holder-teacher div'),
playBtn = document.querySelector('#play-pause');
//play-pause -- I wanted to have a single play-pause button, which is working fine
addEvent(playBtn, 'click', function() {
froogaloop.api('paused', function(paused) {
if(paused) {
froogaloop.api('play');
} else {
froogaloop.api('pause');
}
});
});
...
Now, if I want to say call $f(player).api('pause'); in teacher.js, I get this error: Unable to post message to http://player.vimeo.com. Recipient has origin mydomain. It seems like such a simple problem--I'm not sure if it involves usage of 'this' that is currently beyond me, or if I'm grabbing the Vimeo player embed incorrectly--I did get a lot of "no method .api for this object" in experimenting.
The end goal is that I can create a vimeo player, provide controls (both of these are good), and then use the API to call methods that feed into backbone, including pause, play, and time.
Are there other events, aside from 'click' and user-generated events I can use? Like an event that says another function was called? Seems circuitous...My backbone view looks like this:
pause: function () {
this.player.pauseVideo(); //this is for a YouTube API which works great
//I want to be able to similarly call something like froogaloop.api('pause');
},
Thanks so much--StackOverflow has taught me an amazing amount.
So, if 'ready' event is working, then everything must be setup right. But here is one possible problem.
In:
player = $('iframe#player')[0];
vimeoSetup(player);
You are getting an Object by id 'player' and then, in vimeoSetup(player); passing an Object into $f(player) which is working.
Yet, further down the code in ready function you are passing in to $f(player_id) just an id, not an Object, therefore api calls not working.
You just need to get Object again by player_id and pass it into $f(), or save player = $('iframe#player')[0]; as global var and then call all API methods on it (though can be not a good option, if You want to make something like few dinamicly spawning players each one with own controls, or if You just one of those, who scared of global variables)
I want to write a web extension that will be able to detect playback status of any youtube video a user visits. I looked into the youtube API but it seems like I can only access youtube videos that I've embedded myself. However in this situation, I am not embedding them.
Is there any way to do this?
I've been intrigued by this question since it was asked, and tonight at our local GDG we had a Chrome extension hackathon where I put some experiments to the test. Here's what I've discovered.
A) the extension sandbox really is tough; nothing I tried could get at the javascript coded into the page (if it existed).
B) one solution that works pretty well under certain circumstances is to use a content-script to do code injection, and set up your own listeners on an existing iframe embed. Something like this works:
var regex = /https?:\/\/(?:[0-9A-Z-]+\.)?(?:youtu\.be\/|youtube\.com\S*[^\w\-\s])([\w\-]{11})(?=[^\w\-]|$)(?![?=&+%\w]*(?:['"][^<>]*>|<\/a>))[?=&+%\w]*/ig;
var node, nodes = document.evaluate("(//iframe/#src)",document,null,XPathResult.UNORDERED_NODE_ITERATOR_TYPE,null);
var hasvid=false,vids = [];
while (node=nodes.iterateNext()) { // uses regex to run through DOM, looking for embeds
if (regex.test(node.nodeValue)) {
hasvid=true;
vids.push(node.ownerElement);
}
}
// Now inject a script that sets up a YT.player object and bind it to our extension's functions
if (hasvid) {
playerlines=['function onYouTubePlayerAPIReady() {'];
for (i=0;i<vids.length; i++) {
playerlines.push('player = new YT.Player(\''+vids[i].id+'\', { events: {\'onStateChange\': onPlayerStateChange}});}');
}
callbacklines=['function onPlayerStateChange(event) {',
'if (event.data == YT.PlayerState.ENDED) { alert(\'video is over\'); }',
'else if (event.data == YT.PlayerState.PAUSED) { alert(\'player is paused\'); }',
'else if (event.data == YT.PlayerState.PLAYING) { alert(\'player is playing\'); } }'];
// notify the background page to actually do the injecting of the script
chrome.extension.sendRequest({}, function(response) {});
codelines=playerlines.concat(callbacklines);
var actualCode=codelines.join('\n');
var script = document.createElement('script');
script.textContent = actualCode;
var jsapi = document.createElement('script');
jsapi.src = "http://www.youtube.com/player_api";
(document.head||document.documentElement).appendChild(jsapi);
(document.head||document.documentElement).appendChild(script);
With something like that, you could not just run alerts, but theoretically send all events back to your background page so it could do what it wants based on what the events are.
C) This is of no use if the video wasn't embedded with an iFrame; I tried to employ a similar solution when it was an or element, and while I could detect it just fine, I couldn't get listeners set up to capture the events. I may have been overlooking something, or it may be that the events are fired differently so I couldn't get the binding I needed to.
D) One less-than-useful workaround would be to manipulate the DOM and remove an embedded video, re-embedding it yourself with more direct bindings and listeners set up (as another answer suggested). However, if the original page had been using the API itself, such an action would break its listeners, so there could be unintended consequences if the video you're seeing is part of a API-based web app.
Hope this is useful info.
Well, so embed the videos yourself!
You can use content-scripts to modify the youtube frame before it loads, or even better...
I'm not sure how youtube api works, but you can probably intercept video request with Chrome's Web Request API and alter the request data to get what you want.
If you provide more info, may I can give you more details on how do that...
Good luck!
I'm trying to understand how to target an existing iframe using the YouTube API (i.e. without constructing an iframe with the script).
As usual, Google does not give enough API examples, but explains that it IS possible, here http://code.google.com/apis/youtube/iframe_api_reference.html
Here is an example of what I'm trying to do - the video underneath the thumbnail should play. I am almost there, but only the first video plays...
http://jsfiddle.net/SparrwHawk/KtbYR/2/
TL;DR: DEMO: http://jsfiddle.net/KtbYR/5/
YT_ready, getFrameID and onYouTubePlayerAPIReady are functions as defined in this answer. Both methods can be implemented without any preloaded library. In my previous answer, I showed a method to implement the feature for a single frame.
In this answer, I focus on multiple frames.
HTML example code (important tags and attributes are capitalized, <iframe src id>):
<div>
<img class='thumb' src='http://i2.cdnds.net/11/34/odd_alan_partridge_bio_cover.jpg'>
<IFRAME ID="frame1" SRC="http://www.youtube.com/embed/u1zgFlCw8Aw?enablejsapi=1" width="640" height="390" frameborder="0"></IFRAME>
</div>
<div>
<img class='thumb' src='http://i2.cdnds.net/11/34/odd_alan_partridge_bio_cover.jpg'>
<IFRAME ID="frame2" SRC="http://www.youtube.com/embed/u1zgFlCw8Aw?enablejsapi=1" width="640" height="390" frameborder="0"></IFRAME>
</div>
JavaScript code (YT_ready, getFrameID, onYouTubePlayerAPIReady and the YouTube Frame API script loader are defined here)
var players = {}; //Define a player storage object, to expose methods,
// without having to create a new class instance again.
YT_ready(function() {
$(".thumb + iframe[id]").each(function() {
var identifier = this.id;
var frameID = getFrameID(identifier);
if (frameID) { //If the frame exists
players[frameID] = new YT.Player(frameID, {
events: {
"onReady": createYTEvent(frameID, identifier)
}
});
}
});
});
// Returns a function to enable multiple events
function createYTEvent(frameID, identifier) {
return function (event) {
var player = players[frameID]; // Set player reference
var the_div = $('#'+identifier).parent();
the_div.children('.thumb').click(function() {
var $this = $(this);
$this.fadeOut().next().addClass('play');
if ($this.next().hasClass('play')) {
player.playVideo();
}
});
}
}
In my previous answer, I bound the onStateChange event. In this example, I used the onReady event, because you want to call the functions only once, at initialization.
This example works as follows:
The following methods are defined in this answer.
The YouTube Frame API is loaded from http://youtube.com/player_api.
When this external script has finished loading, onYoutubePlayerAPIReady is called, which in his turn activates all functions as defined using YT_ready
The declaration of the following methods are shown here, but implemented using this answer. Explanation based on the example:
Loops through each <iframe id> object, which is placed right after <.. class="thumb">.
At each frame element, the id is retrieved, and stored in the identifier variable.
The internal ID of the iframe is retrieved through getFrameID. This ensures that the <iframe> is properly formatted for the API. In this example code, the returned ID is equal to identifier, because I have already attached an ID to the <iframe>.
When the <iframe> exists, and a valid YouTube video, a new player instance is created, and the reference is stored in the players object, accessible by key frameID.
At the creation of the player instance, a **onReady* event is defined. This method will be invoked when the API is fully initialized for the frame.
createYTEvent
This method returns a dynamically created function, which adds functionality for separate players. The most relevant parts of the code are:
function createYTEvent(frameID, identifier) {
return function (event) {
var player = players[frameID]; // Set player reference
...
player.playVideo();
}
}
frameID is the ID of the frame, used to enable the YouTube Frame API.
identifier is the ID as defined in YT_ready, not necessarily an <iframe> element. getFrameID will attempt to find the closest matching frame for a given id. That is, it returns the ID of a given <iframe> element, or: If the given element is not an <iframe>, the function looks for a child which is a <iframe>, and returns the ID of this frame. If the frame does not exists, the function will postfix the given ID by -frame.
players[playerID]` refers to the initialized player instance.
Make sure that you also check this answer, because the core functionality of this answer is based on that.
Other YouTube Frame API answers. In these answers, I showed various implementations of the YouTube Frame/JavaScript API.