Monitoring embeded video plays

This week I got a simple request from our customer - to count plays of videos embeded at their site. We currently support different kinds of players - from FLVs interpeted by JwPlayer, Vimeo, Czech Stream.cz to YouTube movies. The task was so simple that I (fool) made a prototype only for FireFox and estimated at most few hours for the implementation. I couldn't have been dumber ...

First attempt

The first and very naive attempt was to attach click handler via jQuery (but this didn't work at all):


$(document).ready(function() {
   $("#video object").live("click", "alert('Played')");
})

All players are embeded by the swfobject.js library and in the latest (2.2) version it is possible to add callback when object is created. I wrote a simle interceptor that added onclick attribute to the DOM HTML object element and it worked! After clicking anywhere at flash my javascript closure was called:


var flashvars = {
   file: "/video/myvideo.flv",
   allowfullscreen: "false",
   allowscriptaccess: "always",
   image: "/img/thumbnail.jpg",
   bufferlength: "0",
   screencolor: "000000"
};
swfobject.embedSWF(
   "/swf/u/jw-flv-player.swf", "video", "509", "382", "8.0.0",
   null, flashvars, { wmode: "transparent", allowFullScreen: "true" }, null,
   function (domObj) {
      $(domObj.ref).attr("onclick", "alert('Played')");
   });

And that was the time when I placed my bet on the realization time. During realization we found out that it didn't work anywhere else (IE, Chrome ...)

Second attempt

While Googling I run at interesting article that seemed to address similar issue as I was trying to solve. The presented solution really works - in all browsers you have transparent area above the flash that consumes clicks anywhere on the flash movie - bingo! But clicks (and mouse movement too) are not propagated to the underlying Flash so that you cannot make the movie play.

I played with it for a while and consulted Google too. According to several sources - it's not possible to propagate clicks through the transparent object below. I threw away all my hopes after reading following articles Forwading mouse events or StackOverflow.

So after that it remained only to do the dirty work.

Third and final solution

By dirty work I mean to study JavaScript APIs of each and every used player and hook on their callbacks. At the time I didn't know whether all of them have suitable API or not. After several hours I made up few lines that did what I needed:


   <!-- JW PLAYER --> 
    <script type="text/javascript">
    //<![CDATA[
        var flashvars = {
           file: "/video/myvideo.flv",
           allowfullscreen: "false",
           allowscriptaccess: "always",
           image: "/img/thumbnail.jpg",
           bufferlength: "0",
           screencolor: "000000"
        };
        swfobject.embedSWF(
           "/swf/u/jw-flv-player.swf", "video",
           "509", "382", "8.0.0", null, flashvars,
           { wmode: "transparent", allowFullScreen: "true", allowScriptAccess: "always" },
           {id: "video", name: "video"}
        );
    //]]>
    </script>
   <!-- YOUTUBE -->
    <script type="text/javascript">
    //<![CDATA[
        swfobject.embedSWF(
           "http://www.youtube.com/v/${video.videoReference}?version=3&enablejsapi=1&playerapiid=video",
           "video", "509", "306", "8.0.0", null, {},
           { wmode: "transparent", allowFullScreen: "true", allowScriptAccess: "always" }
        );
    //]]>
    </script>
   <!-- STREAM.CZ -->
    <script type="text/javascript">
    //<![CDATA[
        swfobject.embedSWF(
           "http://www.stream.cz/object/${video.videoReference}",
           "video", "509", "311", "8.0.0", null, {},
           { wmode: "transparent", allowFullScreen: "true", allowScriptAccess: "always" }
        );
    //]]>
    </script>
   <!-- VIMEO -->
    <script type="text/javascript">
    //<![CDATA[
        swfobject.embedSWF(
           "http://vimeo.com/moogaloop.swf?clip_id=${video.videoReference}&server=vimeo.com&fullscreen=1&api=1&player_id=video",
           "video", "509", "311", "8.0.0", null, {},
           { wmode: "transparent", allowFullScreen: "true", allowScriptAccess: "always" }
        );
    //]]>
    </script>

Important parts are allowScriptAccess: "always" and enabling javascript API (YouTube version=3&enablejsapi=1, Vimeo api=1, no parameters are needed for JwPlayer - it has API always enabled).

Then you need following javascript functions in place (all functions simply trigger click event of the parent div - there sits an onclick handler that performs AJAX call to increase play counter on the server side):


function playerReady(thePlayer) {
    var jwPlayer = $("#" + thePlayer.id);
    jwPlayer.get(0).addControllerListener("ITEM", "$('#" + thePlayer.id + "').parent('div').click()");
}
function vimeo_player_loaded(playerId) {
    var vimeoPlayer = $("#" + playerId);
    vimeoPlayer.get(0).api_addEventListener("onPlay", "$('#" + playerId + "').parent('div').click()");
}
function onYouTubePlayerReady(playerId) {
  var ytplayer = $("#" + playerId);
  ytplayer.get(0).addEventListener("onStateChange", "function(state) { if (state == 1) { $('#" + playerId + "').parent('div').click() } }");
}

Luckily each player uses similar way of interaction. When JavaScript API is enabled and player initializes itself it calls some JS method with certain name (Youtube onYouTubePlayerReady, Vimeo vimeo_player_loaded, JwPlayer playerReady, Stream.cz playerReady). Then you need only to get the HTML Object reference and call method on the Flash interface to register your listener. Each player has its own method that does the same - first argument states the event type, second argument (String) denotes the JavaScript code, that is executed when event is triggered.

It's a pitty I was not able to make Stream.cz API to work. Though I decompiled their Flash code (API is not documented at all) and really thought I found a way how attach to it, it didn't call my listeners back. And I really have better things to do than to fiddle with this for hours.

Disclaimer

Feel free to use the code. Let's hope you save some time I had to spent to make all this work.