Dispatching YouTube API events to individual JavaScript objects

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.

If you’ve enjoyed this article, you might enjoy my recently published book JavaScript Testing Recipes. It’s full of simple techniques for writing modular, maintainable JavaScript apps in the browser and on the server.