Friday, July 1, 2011

jQuery Deferred In Use

Since jQuery 1.5, Deferred was introduced in the framework and the whole Ajax module was rewritten with it. Deferred is implemented based on the Promise/A design. I had heard of this concept before but never paid much attention to it until jQuery, which I use a lot of, implemented it. I realized the power of it, but never got a chance to use it until now.

Now I am faced with a problem (probably a design flaw from the beginning). I am using a jQuery plugin called jPlayer, which is a great library for playing sound on web pages. In order to simply the use of jPlayer, I decided to write a method that will play sound for a given sound URL path, so that everyone using it will just have to give the path of the sound file to play and not have to worry about anything else. So, on every page that uses jPlayer, I instantiate/setup an instance of jPlayer so it can be reused (only one sound will be played at once) for every sound file.
Code looks like:
$(function() {
  var $jp = $("#jplayer");
  if ($jp.length == 0) {
    $jp = $("<div id="jplayer"></div>");
    $("body").append($jp)
  }
  
  $jp.jPlayer({
      "swfPath" : "../js",
      "solution" : "flash, html",
      "ready" : function () {
        console.log("player ready")
        //setMedia must be called here
      },
      "supplied" : "mp3",
      "preload" : "none" //auto
  });
});

function playMusic(musicSrc) {
  console.log("play music: "+musicSrc);
  $("#jplayer").jPlayer("setMedia", {"mp3" : musicSrc}).jPlayer("play"); 
}
The method playMusic is exposed for use.
({"solution" : "flash, html"} is there to make sure FF uses Flash. In most IE, it will use Flash automatically.)

Notice the "ready" parameter in jPlayer's constructor. It is a callback for the constructor. The reason for it is because in some browser Flash is required to play sound file, and the loading time of Flash is unknown and asynchronous. The callback is to tell you when the Flash is ready then you can play your sound. If is not ready and you play, nothing will happen and you're left in the dark to wonder why.
This worked fine for most of the cases, since, up and until I see my problem, all my use cases were that user will click something to trigger the play. And by that time, Flash is most likely to be loaded. Yes, it's a dangerous assumption.

Now the problem comes when I need to play sound when the page finished loading. If I simply add a line like this
window.onload = function(){
  playMusic("../images/applause.mp3");
}
or simply
playMusic("../images/applause.mp3");

These don't work.
For the first case, Flash is not necessarily ready when playMusic is called.
The second case has the following output from Firebug and obvious don't work:
play music: ../images/applause.mp3
player ready
 Now, without changing any API, I need a way to get my function or sound played when the Flash is ready. Here is where Deferred comes into play.
I simply has to instantiate an instance of Deferred and let my playMusic passes every call to the Deferred.
And resolves the Deferred when Flash is ready. This way all calls to playMusic will queued up in the Deferred before it is resolved, and if it is resolved, the function will be called immediately. Also, this way I don't have to worry about when and where I am calling playMusic.
Here is the new code:
var jplayerDeferred = $.Deferred();
$(function() {
  var $jp = $("#jplayer");
  if ($jp.length == 0) {
    $jp = $("<div id="jplayer"></div>");
    $("body").append($jp)
  }
  
  $jp.jPlayer({
      "swfPath" : "../js",
      "solution" : "flash, html",
      "ready" : function () {
        console.log("player ready")
        //setMedia must be called here
        jplayerDeferred.resolve();
      },
      "supplied" : "mp3",
      "preload" : "none" //auto
  });
  
});

function playMusic(musicSrc) {
    
  jplayerDeferred.done(function(){
    console.log("play music: "+musicSrc);
    $("#jplayer").jPlayer("setMedia", {"mp3" : musicSrc}).jPlayer("play"); 
  });

}

Then I can just call it like this right after.

playMusic("../images/applause.mp3");

Now as soon as the Flash gets loaded, the sound is played. Here is the output from Firebug.
player ready
play music: ../images/applause.mp3