Last week, I had the enviable task of creating a skinnable version of the YouTube player using JavaScript; something that would drop some HTML into the page that could be styled using CSS. Naturally, I wanted to package it up as a reusable class so you could, if required, create several videos on a single page without hard-coding the names of callback functions and the like.
Those of you that have used the chromeless YouTube player API will know how
painful this is. First up, it requires that you implement a global event handler
called onYouTubePlayerReady()
that fires when a single player’s Flash
envorionment is done loading. This, at least, has the decency to pass you the
player’s element ID so you can figure out which player is ready. The real pain
starts when you want to support the other events – unstarted, ended, playing,
paused and cued – that occur once the player is active. This is what I had in
mind:
var player = new YoutubePlayer('elementId', 'videoId');
player.on('paused', function(player) {
// react to pause event
});
It may help to keep that in mind as we go through the implementation. Finding out when each player is ready is simple enough. Let’s set up a skeleton class that remembers all its instances and can retrieve them by ID (we’re using Ojay and JS.Class):
var YoutubePlayer = JS.Class({
initialize: function(elementId, videoId) {
this._id = elementId;
this.klass._register(this);
// setup up player UI...
},
_onready: function() {
// to be implemented
},
extend: {
_instances: [],
_register: function(player) {
this._instances.push(player);
},
findById: function(id) {
return this._instances.filter({_id: id})[0];
}
}
});
onYouTubePlayerReady = function(id) {
YoutubePlayer.findById(id)._onready();
};
So the class maintains an list (_instances
) of all its intances, and when the
class is instantiated the new object is pushed onto this list. We have a class
method (findById
) that returns the instance with the given ID, and the global
YouTube ready handler can then notify individual instances when they are ready.
Now to get the other events working. YouTube has these weird cryptic numeric IDs for states, so let’s map them to sensible names:
YoutubePlayer.STATE_NAMES = {
unstarted: -1,
ended: 0,
playing: 1,
paused: 2,
buffering: 3,
cued: 5
};
For notifying you of these state changes, YouTube provides the following convenient method:
ytplayer.addEventListener('onStateChange', 'ytcallback');
The pain comes in when you find out you cannot pass a function reference in as
an event handler. You have a to pass a string ('ytcallback'
above) that tells
the Flash environment the name of a global handler. The handler will be passed
the numeric state ID, but not the player ID that fired the event, for example:
ytcallback = function(eventId) {
// do something with event
};
That’s all you have to work with. Fortunately, I found out that you can pass in
a string of code as the callback argument, and Flash will eval it to get a
function reference. So, we’ll be able to dispatch events if we create a global
function that takes a player ID and event ID, and curry this function to create
functions that just take the event ID but remember which player fired the event.
Let’s add it as a globally-accessible class method on YoutubePlayer
:
YoutubePlayer._dispatchEvent = function(pId, eId) {
var player = YoutubePlayer.findById(pId);
player._notifyEvent(eId);
}.curry();
This function finds the player with the right ID, and sends it a signal that a particular event has happened – it is then up to the player to handle the event. The function is curried, so if we call it with only the first argument we get back another function that just takes the event ID.
Now we can finally wire up the event dispatch methods. The first step is to
mix-in Ojay.Observable
(available in the upcoming 0.2 release), which extends
JS.Observable
and provides the on(eventName)
method:
YoutubePlayer.include(Ojay.Observable);
When each player is ready, we can add an event listener using our curried class
method. The instance method notifyEvent()
will take the event ID and convert
it to an event name, then notify any observers of the event:
YoutubePlayer.include({
_onready: function() {
var ytplayer = Ojay.byId(this._id).node;
var callback = 'YoutubePlayer._dispatchEvent' +
'("' + this._id + '")';
ytplayer.addEventListener('onStateChange', callback);
},
_notifyEvent: function(eventId) {
for (var key in this.klass.STATE_NAMES) {
if (this.klass.STATE_NAMES[key] == eventId)
this.notifyObservers(key);
}
}
});
When the YouTube player evals the callback
string it will get back a function
that accepts the event ID and has pre-stored the player ID so the event gets
dispatched to the right YoutubePlayer
instance.
So we finish up with our completed class looking like:
var YoutubePlayer = JS.Class({
include: Ojay.Observable,
initialize: function(elementId, videoId) {
this._id = elementId;
this.klass._register(this);
// setup up player UI...
},
_onready: function() {
var ytplayer = Ojay.byId(this._id).node;
var callback = 'YoutubePlayer._dispatchEvent' +
'("' + this._id + '")';
ytplayer.addEventListener('onStateChange', callback);
},
_notifyEvent: function(eventId) {
for (var key in this.klass.STATE_NAMES) {
if (this.klass.STATE_NAMES[key] == eventId)
this.notifyObservers(key);
}
},
extend: {
_instances: [],
_register: function(player) {
this._instances.push(player);
},
findById: function(id) {
return this._instances.filter({_id: id})[0];
},
_dispatchEvent: function(pId, eId) {
var player = YoutubePlayer.findById(pId);
player._notifyEvent(eId);
}.curry(),
STATE_NAMES: {
unstarted: -1,
ended: 0,
playing: 1,
paused: 2,
buffering: 3,
cued: 5
}
}
});
onYouTubePlayerReady = function(id) {
YoutubePlayer.findById(id)._onready();
};
There’s not much to it in the end, it just took a little while to get there. Obviously this class still needs the UI implementation adding, this is just supposed to demonstrate the event dispatching code. I’m not sure whether this solution just applies to YouTube’s Flash environment or to Flash APIs in general; if any other applications for it come up I’ll post an update here.