I want to instantiate a new MediaElementPlayer object. When it's successfully created, I want to pass the whole object on to another function (my_object.attachEvents). My code is as follows:
var options = {
success: function () {
//point 2
console.log("passing player object", local_player_instance);
my_main_object.attachEvents(local_player_instance);
}
}
//point 1
console.log('player inited', local_player_instance);
local_player_instance.video = new MediaElementPlayer('#video_player', options);
my_main_object.attachEvents = function(local_player_instance) {
local_player_instance.video.play()
}
In Firefox, the assignment at point one is executed before the line at point 2 calls the attach events method.
Im Chrome, point 2 is evaluate first, and as a result when the play method in the attach events function is called it doesn't exist.
My question is, how do I pass successfully pass the MediaElementPlayer to another function when it is created?
The best way to handle this in a cross browser way is
// here's where you'll store a global reference to the player
var globalMediaElement = null;
var options = {
success: function (domNode, mediaElement) {
globalMediaElement = mediaElement;
doStuff();
// you can also get the the player via jQuery here
$('#video_player').player
}
}
// create MediaElement
new MediaElementPlayer('#video_player', options);
function doStuff() {
globalMediaElement.play();
}
Our plugin function is something like: var pluginName = function(selector, settings){}. So user call it like var myPlugin = new pluginName('#id', {settings}). But how to detect that user called it again, for example, with new options: myPlugin = new pluginName('#id', {settings_2})?
We need to detect this to prevent double events attaching: something like destroy created object/remove events and call init again.
So problem is we dont have access to myPlugin variable to check it.
Just put the plugin around it's own scope:
var pluginName = (function(){
var plugin_created = false;
var plugin = function(selector, settings){
if(plugin_created) return false;
plugin_created = true;
//rest of the plugin class
}
return plugin;
})();
So when you create it for the first time it will set it's local scope variable to true and then any other creations will fail.
When I do a console trace on the positionSlides method its showing slideShow as undefined.
How can this be when I clearly instantiate it in the document.ready callback. I also make sure to make this variable global so both the slideShow and the slideShowNavigation would have access to both these variables.
var slideShow, slideShowNavigation;
$(document).ready(function(){
slideShow = new SlideShow( $('#header #slideshow'), 980 );
slideShowNavigation = new SlideShowNavigation( $('#header').find("#leftArrow"), $('#header').find("#rightArrow") );
});
// SLIDE SHOW CLASS
function SlideShow( divContainer, slideWidth ){
// Check to make sure a new instance is created
if( ! (this instanceof SlideShow) ) return new SlideShow();
this.$imageContainer = divContainer;
this.slideWidth = slideWidth;
var maxImages = this.$imageContainer.children().length;
this.getMaxSlides = function(){
return maxImages;
}
this.positionSlides();
}
SlideShow.prototype.positionSlides = function(){
console.log('imageContainer = '+slideShow);
}
SlideShow.prototype.update = function( dir ){
}
// ARROW NAVIGATION FOR SLIDESHOW
function SlideShowNavigation( left, right){
if( ! (this instanceof SlideShowNavigation) ) return new SlideShowNavigation();
//this.updateArrows( slideShow.$imageContainer.find(":first") );
}
SlideShowNavigation.prototype.updateArrows = function( item ){
}
The problem is this:
slideShow = new SlideShow( $('#header #slideshow'), 980 );
That line calls your SlideShow function, which calls positionSlides(), which looks for slideShow, but slideShow isn't defined until your SlideShow function returns.
Instead, in positionSlides() use this instead of slideShow to refer to the instance (this way your class is independent of the variable name.
slideShow is not defined until the SlideShow function returns that definition. SlideShow.positionSlides is called before the constructor finishes.
Fix: call positionSlides only after slideShow has been defined.
var slideShow, slideShowNavigation;
$(document).ready(function(){
slideShow = new SlideShow( $('#header #slideshow'), 980 );
slideShow.positionSlides();
slideShowNavigation = new SlideShowNavigation( $('#header').find("#leftArrow"), $('#header').find("#rightArrow") );
});
// SLIDE SHOW CLASS
function SlideShow( divContainer, slideWidth ){
// Check to make sure a new instance is created
if( ! (this instanceof SlideShow) ) return new SlideShow();
this.$imageContainer = divContainer;
this.slideWidth = slideWidth;
var maxImages = this.$imageContainer.children().length;
this.getMaxSlides = function(){
return maxImages;
}
}
SlideShow.prototype.positionSlides = function(){
console.log('imageContainer = '+slideShow);
}
SlideShow.prototype.update = function( dir ){
}
// ARROW NAVIGATION FOR SLIDESHOW
function SlideShowNavigation( left, right){
if( ! (this instanceof SlideShowNavigation) ) return new SlideShowNavigation();
//this.updateArrows( slideShow.$imageContainer.find(":first") );
}
SlideShowNavigation.prototype.updateArrows = function( item ){
}
The problem is that you are defining what slideshow is by calling the function Slideshow. That function calls positionSlides. This is called before the object is done constructing.
You have called positionSlides from the Slideshow constructor function. The slideShow variable is not set until after that constructor function finishes. So, you're accessing the slideShow global variable before it has been set in this part of positionSlides:
console.log('imageContainer = '+slideShow);
I'd suggest that in the positionSlides method, you could probably reference "this" instead of the global variable.
Change that line to:
console.log('imageContainer = ' + this);
As a suggestion, I find it a bit dangerous to use variable names that differ only in case slideShow (a global variable name) and SlideShow (a function name) and slideshow (a CSS identifier). It makes your code harder to read and understand and it can be easy to inadvertently make a mistake. In this case, I'd suggest the global variable be something like "theSlideShow" for increased readability and less likelihood of mistake.
The SlideShow object calls the method positionSlides in its constructor:
this.positionSlides();
This method tries to access the variable that you are assigning itself to:
console.log('imageContainer = '+slideShow);
It doesn't exist yet because the constructor hasn't finished.
(edit - per discussion about relationships between the two objects)
"SlideShow needs to call methods of SlideShowNavigation" - done using events in Java.
You can use callbacks in javascript to create an event-driven model.
Suppose that SlideShowNavigation has a dependency of a SlideShow object, in the navigation. Also note the use of a single options object parameter, instead of individual parameters, this is a common construct because it simplifies parameter passing, resulting in more readable, more easily maintainable code. You don't risk breaking something if you add/remove a parameter since they are not positionally dependent this way.
function SlideShow() {
// setup code
this.onSomethingHappened=null;
}
SlideShow.prototype.doSomething() {
// do something
/// if a callback exists & is defined properly, then invoke it
if (this.onSomethingHappened &&
typeof this.onSomethingHappened==='function') {
this.onSomethingHappened(this);
}
}
SlideShow.prototype.publicMethod() {
}
function SlideShowNavigation(options){
if( ! (this instanceof SlideShowNavigation) )
return new SlideShowNavigation();
this.slideShow = options.slideShow
this.left = options.left;
this.right = options.right;
// bind to callbacks (events): assigning the `onSomethingHappened` property
// of the SlideShow to a function in this object, which it will call at the
// appropriate time
this.slideShow.onSomethingHappened = this._somethingHappened;
//this.updateArrows( slideShow.$imageContainer.find(":first") );
}
// i use an underscore to indicate a private method - since there's
// no way to create non-public prototype members. You can can
// alternatively create private functions in the constructor though
// it's less memory efficient, and they are not directly accessible
// from prototype methods.
SlideShowNavigation.prototype._somethingHappened(e) {
// e is the sender of the event
...
// you can invoke methods of the sender directly using the ref passed here,
// if you like
e.publicMethod();
}
First off, I know I can copy "this" on instantiation, but that doesn't work here.
Basically I'm writing something to track people interacting with Youtube videos.
I got this working fine for one video at a time. But I want it to work on pages with multiple Youtube videos as well, so I converted the code to a class so I can create a new instance of it for each video on the page.
The problem is when trying to bind to the Youtube event listener for state changes. For "non-class" code, it looks like this:
var o = document.getElementById( id );
o.addEventListener("onStateChange", "onPlayerStateChange" );
(onPlayerStateChange being the function I wrote to track state changes in the video)
(I'm also aware that addEventListener won't work with MSIE but I'm not worrying about that yet)
But when I'm inside a class, I have to use "this" to refer to another function in that class. Here's what the code looks like:
this.o = document.getElementById( id );
this.o.addEventListener("onStateChange", "this.onPlayerStateChange" );
When it's written like this, this.onPlayerStateChange is never called. I've tried copying "this" into another variable, e.g. "me", but that doesn't work either. The onPlayerStateChange function is defined within the "this" scope before I do this:
var me = this;
this.o = document.getElementById( id );
this.o.addEventListener("onStateChange", "me.onPlayerStateChange" );
Any insights?
Looking through other similar questions here, all of them are using jQuery, and I think doing it that way might work if I did it that way. But I don't want to use jQuery, because this is going to be deployed on random third party sites. I love jQuery but I don't want it to be a requirement to use this.
You need a global way to access the onPlayerStateChange method of your object. When you assign me as var me = this;, the variable me is only valid inside the object method where it is created. However, the Youtube player API requires a function that is accessible globally, since the actual call is coming from Flash and it has no direct reference to your JavaScript function.
I found a very helpful blog post by James Coglan in which he discussed a nice way to communicate with the Youtube's JavaScript API and manage events for multiple videos.
I have released a JavaScript wrapper library using his ideas at http://github.com/AnuragMishra/YoutubePlayer. Feel free to checkout the code. The underlying idea is simple - store all instances of the player object on the constructor. For example:
function Player(id) {
// id of the placeholder div that gets replaced
// the <object> element in which the flash video resides will
// replace the placeholder div and take over its id
this.id = id;
Player.instances.push(this);
}
Player.instances = [];
When passing a string as a callback, use a string of the form:
"Player.dispatchEvent('playerId')"
When the flash player evals this string, it should return a function. That function is the callback that will ultimately receive the playback event id.
Player.dispatchEvent = function(id) {
var player = ..; // search player object using id in "instances"
return function(eventId) { // this is the callback that Flash talks to
player.notify(eventId);
};
};
When the flash player has loaded the video, the global onYoutubePlayerReady function is called. Inside that method, setup the event handlers for listening to playback events.
function onYouTubePlayerReady(id) {
var player = ..; // find player in "instances"
// replace <id> with player.id
var callback = "YoutubePlayer.dispatchEvent({id})";
callback = callback.replace("{id}", player.id);
player.addEventListener('onStateChange', callback);
}
See a working example here..
You can use a technique called currying to achieve this. For that you need a currying function. Here's one I wrote some time back
/**
* Changes the scope of function "fn" to the "scope" parameter specified or
* if not, defaults to window scope. The scope of the function determines what
* "this" inside "fn" evaluates to, inside the function "fn". Any additional arguments
* specified in this are passed to the underlying "curried" function. If the underlying
* function is already passed some arguments, the optional arguments are appended
* to the argument array of the underlying function. To explain this lets take
* the example below:
*
* You can pass any number of arguments that are passed to the underlying (curried)
* function
* #param {Function} fn The function to curry
* #param {Object} scope The scope to be set inside the curried function, if
* not specified, defaults to window
* #param arguments {...} Any other optional arguments ot be passed to the curried function
*
*/
var curry = function(fn, scope /*, arguments */) {
scope = scope || window;
var actualArgs = arguments;
return function() {
var args = [];
for(var j = 0; j < arguments.length; j++) {
args.push(arguments[j]);
}
for(var i = 2; i < actualArgs.length; i++) {
args.push(actualArgs[i]);
}
return fn.apply(scope, args);
};
};
You can use it to curry other functions and maintain the 'this' scope inside the functions.
Check out this article on currying
this.o.addEventListener("onStateChange", curry(onPlayerStateChange, this));
Edit:
var curriedFunc = curry(onPlayerStateChange, this);
this.o.addEventListener("onStateChange", "curriedFunc");
Edit:
Okay lets say this is your custom class you create:
function MyCustomClass() {
var privateVar = "x"; // some variables;
this.onPlayerStateChange = function() { //instance method on your custom class
// do something important
}
}
On a global level you create an instance of MyCustomClass
var myCustom = new MyCustomClass(); // create a new instance of your custom class
var curriedFunc = curry(myCustom.onplayerStageChange, myCustom); // curry its onplayerstateChange
// now add it to your event handler
o.addEventListener("onStateChange", "curriedFunc");
You should be using the following to attach an event:
this.o.addEventListener("statechange", this.onPlayerStateChange);
For addEventListener, you don't need to add the on prefix.
What I posted above is correct for standard javascript, but because this passes it to the YT flash object, it's expecting onStateChange which is correct.
HTH
EDIT: Try the method in this post to help.
TheCloudlessSky was partly right and Sean was partly right. You can continue to use "onStateChange" as the event name, but don't put this.onPlayerStateChange in quotations - doing so removes the special meaning of this and javascript will look for a function named "this.onPlayerStateChange" rather than looking for a function "onPlayerStateChange" within this object.
this.o.addEventListener("onStateChange", this.onPlayerStateChange);
After looking at the Youtube Api, it looks like the addEventListener only accepts a String for the event handler function. That means there's no clean way to register a unique event handler for each object.
An alternative is to register a global handler for all youtube state changes, and then let that handler pass the state change onto all your objects. Assuming you have an array of "tracker" objects:
function globalOnPlayerStateChange() {
for (tracker in myTrackerObjects) {
tracker.playerStateChange();
}
}
Each tracker object can then figure out by itself whether or not a state change actually occured (using the API's getPlayerState function):
function MyYoutubeTracker() {
this.currentState = ...
// Determine if state changed happened or not
this.playerStateChange = function() {
var newState = this.o.getPlayerState();
if (newState != this.currentState) {
// State has changed
this.currentState = newState;
}
}
// Register global event handler for this youtube object
this.o.addEventListener("onStateChange", "globalOnPlayerStateChange");
}
Ok, I got this all working. It's a bit of an ugly hack but it works. Basically I'm storing each new instance of the class in an array, and I'm passing the array key (1, 2, etc) into the class, so it can refer to itself externally as needed in a few key places.
The places I need the class to refer to itself externally are the string I pass to addEventListener, and within a few setTimeout functions, where "this" apparently loses its context (as far as I can tell anyways, because the only way I could them working was changing "this" to use external references instead.
Here's the full code.
On the page that has Youtube videos, they are injected using swfobject. The _ytmeta object stores the titles for each video. It's optional, but it's the only way to log the title of a video, because Youtube's API does not give it to you. This means you have to know the title up front, but the point is simply that if you want the title to show up in our reports, you have to create this object:
<div id='yt1'></div>
<script src='youtube.js'></script>
<script src='swfobject.js'></script>
<script>
var _ytmeta = {}
_ytmeta.yt1 = { 'title': 'Moonwalking in Walmart' };
var params = { allowScriptAccess: "always" };
swfobject.embedSWF("http://www.youtube.com/v/gE1ZvCnwkYk?enablejsapi=1&playerapiid=yt1", "yt1", "425", "356", "8", null, null, params );
</script>
So we're including the swfobject javascript code, as well as the youtube.js file, which will be hosted on our server and included on the pages you want to track videos.
Here are the contents of youtube.js:
// we're storing each youtube object (video) in an array, and passing the array key into the class, so the class instance can refer to itself externally
// this is necessary for two reasons
// first, the event listener function we pass to Youtube has to be globally accessible, so passing "this.blah" doesn't work
// it has to be passed as a string also, so putting "this" in quotes makes it lose its special meaning
// second, when we create timeout functions, the meaning of "this" inside that function loses its scope, so we have to refer to the class externally from there too.
// _yt is the global youtube array that stores each youtube object. yti is the array key, incremented automatically for each new object created
var _yt = [], _yti = 0;
// this is the function the youtube player calls once it's loaded.
// each time it's called, it creates a new object in the global array, and passes the array key into the class so the class can refer to itself externally
function onYouTubePlayerReady( id ) {
_yti++;
_yt[ _yti ] = new _yta( id, _yti );
}
function _yta( id, i ) {
if( !id || !i ) return;
this.id = id;
this.mytime;
this.scrubTimer;
this.startTimer;
this.last = 'none';
this.scrubbing = false;
this.o = document.getElementById( this.id );
this.o.addEventListener("onStateChange", "_yt["+i+"].onPlayerStateChange" );
this.onPlayerStateChange = function( newState ) {
// some events rely on a timer to determine what action was performed, we clear it on every state change.
if( this.myTime != undefined ) clearTimeout( this.myTime );
// pause - happens when clicking pause, or seeking
// that's why a timeout is used, so if we're seeking, once it starts playing again, we log it as a seek and kill the timer that would have logged the pause
// we're only giving it 2 seconds to start playing again though. that should be enough for most users.
// if we happen to log a pause during the seek - so be it.
if( newState == '2' ) {
this.myTime = setTimeout( function() {
_yt[i].videoLog('pause');
_yt[i].last = 'pause';
_yt[i].scrubbing = false;
}, 2000 );
if( this.scrubbing == false ){
this.last = 'pre-scrub';
this.scrubbing = true;
}
}
// play
else if( newState == '1' ) {
switch( this.last ) {
case 'none':
this.killTimers();
this.startTimer = setInterval( this.startRun, 200 );
break;
case 'pause':
this.myTime = setTimeout( function() {
_yt[i].videoLog('play');
_yt[i].last = 'play';
}, 2000 );
break;
case 'pre-scrub':
this.killTimers();
this.scrubTimer = setInterval( this.scrubRun, 200 );
break;
}
}
// end
else if( newState == '0' ) {
this.last = 'none';
this.videoLog('end');
}
}
// have to use external calls here because these are set as timeouts, which makes "this" change context (apparently)
this.scrubRun = function() {
_yt[i].videoLog('seek');
_yt[i].killTimers();
_yt[i].last = 'scrub';
_yt[i].scrubbing = false;
}
this.startRun = function() {
_yt[i].videoLog('play');
_yt[i].killTimers();
_yt[i].last = 'start';
}
this.killTimers = function() {
if( this.startTimer ) {
clearInterval( this.startTimer );
this.startTimer = null;
}
if( this.scrubTimer ){
clearInterval( this.scrubTimer );
this.scrubTimer = null;
}
}
this.videoLog = function( action ) {
clicky.video( action, this.videoTime(), this.videoURL(), this.videoTitle());
}
this.videoTime = function() {
return Math.round( this.o.getCurrentTime() );
}
this.videoURL = function() {
return this.o.getVideoUrl().split('&')[0]; // remove any extra parameters - we just want the first one, which is the video ID.
}
this.videoTitle = function() {
// titles have to be defined in an external object
if( window['_ytmeta'] ) return window['_ytmeta'][ this.id ].title || '';
}
}
Hopefully, someone in the future will find this helpful, because it was a serious pain in the ass to get it working!
Thank you everyone who posted their ideas here. :)
I'm trying to create a robust audio player in javascript (& jQuery). I know that there are other players out there, but I'd like to try creating my own (so please don't refer me to jquery plugins). This is essentially what I would like to do:
Main.js:
var player = new Player(AudioObj); // Audio object links to Audio class (not shown)
player.buttons.play = $('play');
player.buttons.pause = $('pause'); // Play and pause ID's link to HTML Document Element
Player.js:
Player = function(Audio) {
this.Audio = Audio;
this.buttons = {};
for(var button in this.buttons) {
button.live('click', this.button); // This is the line I Have NO idea about..
}
}
Player.prototype = {
play : function() {
// Do Something
},
pause : function() {
// Do something
}
}
So essentially, I would like the properties to be pre-linked to object functions when you initialize the Player, and to just have it work when I link it to an HTML element.
Thanks!
Matt Mueller
I think this would be a more OO way to go. Setup two more functions inside Player. One function would register a UI element to a Player action and another to unregister the action. So rather than keeping an explicit button collection you can just lean on jQuery.live and jQuery.die. For example:
function registerAction(selector, action) {
// you could have some logic to map the passed in action
// to the actual function name
$(selector).live('click', action/functionName);
}
function unRegisterAction(selector, [action]) {
// you could have some logic to map the passed in action
// to the actual function name
$(selector).die('click', [action/functionName]);
}
Then, your main.js example from above would become:
var player = new Player(AudioObj); // Audio object links to Audio class (not shown)
player.registerAction('#play', play);
player.registerAction('#pause', pause); // Play and pause ID's link to HTML Document Element
And your Player constructor would become:
Player = function(Audio) {
this.Audio = Audio;
}
Or something like that.
This is not the perfect solution but I find it to be pretty elegant:
Player.js
Player.prototype = {
init: function() {
var Player = this;
// Attach buttons to respected functions
for(var button in this.buttons) {
if(typeof Player[button] === "function")
$(this.buttons[button]).bind('click', {Player : this}, Player[button]);
}
},
play: function(e){
var Player = e.data.Player;
var Audio = Player.Audio;
Audio.play();
},
pause: function(e){
var Player = e.data.Player;
var Audio = Player.Audio;
Audio.pause();
}
}
Main.js
var audio = new AudioCore("UpToYou.mp3");
var player = new Player(audio);
player.buttons.play = $('#play');
player.buttons.pause = $('#pause');
player.init();
This provides a nice way to link buttons to the function without passing in a huge array or providing a bunch of options. I would be VERY happy to have a solution that would NOT require you to call init().