Tracking JW Player Events with SiteCatalyst

I recently had a client with a Drupal site who wanted to be able to track whether or not his site members were watching videos on his site completely, and also wanted to know at what point site members were leaving the videos.

The client currently uses SiteCatalyst for analytics, and is using the JW Media Player. After a lot of research, I came across this blog post, which gave some terrific insight regarding how to connect events in the JW Media Player to Omniture. However, the code samples did not apply to SiteCatalyst. The author of the blog post suggested using the s.tl() function in the SiteCatalyst code to track events.

After a lot more research, I finally came up with a solution that tracks when users play a video, pause a video, watch a video until the end, and if the user navigates away from the video before it has completed. The current time of the video is also tracked for each of these events.

Using this methodology, one could easily extend the tracking to include other events such as seek and playlist events. My code is below.

JavaScript:
  1. // Detect if the user navigates away from the video
  2. window.onbeforeunload = confirmExit;
  3.  
  4. var currentPosition = 0;
  5. var currentVolume = 0;
  6. var currentMute = false;
  7. var currentState = "NONE";
  8. var defaultState = "NONE";
  9. var clipduration = 0;
  10. var player = null;
  11. var s = null;
  12.  
  13. function playerReady() {
  14.   player = document.getElementById('swfobject-1');
  15.   addListeners(player);
  16. }
  17.  
  18. function addListeners(player) {
  19.   if (player) {
  20.     addAllModelListeners(player);
  21.   }
  22.   else {
  23.     setTimeout("addListeners("+player+")",100);
  24.   }
  25. }
  26.  
  27. function addAllModelListeners(player) {
  28.   if (typeof player.addModelListener == "function") {
  29.     player.addModelListener("BUFFER", "doNothing");
  30.     player.addModelListener("ERROR", "doNothing");
  31.     player.addModelListener("LOADED", "doNothing");
  32.     player.addModelListener("META", "doNothing");
  33.     player.addModelListener("STATE", "stateListener");
  34.     player.addModelListener("TIME", "positionListener");
  35.   }
  36. }
  37.  
  38. function doNothing(obj) { //nothing
  39. }
  40.  
  41. function positionListener(obj) {
  42.   currentPosition = obj.position;
  43.   clipduration = obj.duration;
  44. }
  45.  
  46. function stateListener(obj) {
  47.   s = s_gi('sitecatalyst_id');
  48.   currentState = obj.newstate;
  49.   switch(obj.newstate) {
  50.     case 'PLAYING':
  51.       s.linkTrackVars="prop1,eVar1,events";
  52.       s.linkTrackEvents="video_play";
  53.       s.prop1=secondsToMinutes(currentPosition);
  54.       s.eVar1=secondsToMinutes(currentPosition);
  55.       s.events="video_play";
  56.       s.tl(this,'o','Play Video');
  57.       break;
  58.     case 'PAUSED':         
  59.       s.linkTrackVars="prop1,eVar1,events";
  60.       s.linkTrackEvents="video_pause";
  61.       s.prop1=secondsToMinutes(currentPosition);
  62.       s.eVar1=secondsToMinutes(currentPosition);
  63.       s.events="video_pause";
  64.       s.tl(this,'o','Pause Video');
  65.       break;
  66.     case 'COMPLETED':
  67.       s.linkTrackVars="prop1,eVar1,events";
  68.       s.linkTrackEvents="video_complete";
  69.       s.events="video_complete";
  70.       s.tl(this,'o','Video Complete');
  71.       break;
  72.  
  73.   }
  74. }
  75.  
  76. function confirmExit() {
  77.   if(currentState != 'COMPLETED') {
  78.     s.linkTrackVars="prop1,eVar1,events";
  79.     s.linkTrackEvents="video_leave";
  80.     s.prop1=secondsToMinutes(currentPosition);
  81.     s.eVar1=secondsToMinutes(currentPosition);
  82.     s.events="video_leave";
  83.     s.tl(this,'o','Leave Video');
  84.     currentState = '';
  85.   }
  86. }
  87.  
  88. // Helper function to convert seconds to mm:ss format
  89. function secondsToMinutes(seconds) {
  90.   // Parse the minutes
  91.   minVar = parseInt(Math.floor(seconds/60));
  92.   minVar = minVar <10 ? '0' + minVar : minVar;
  93.  
  94.   // Parse the seconds
  95.   secVar = parseInt(seconds % 60);              // The balance of seconds
  96.   secVar = secVar <10 ? '0' + secVar : secVar;
  97.  
  98.   return minVar + ':' + secVar;
  99. }

Resources:

Channeling Steve Wilson

I have been a big fan of Candian musician Devin Townsend for a long time. If you're not familiar with his music, it's somewhat difficult to describe. First of all, it's definitely metal and mostly heavy. However, what makes Devin Townsend's music different is that he layers track upon track and ends up with a solid thick sonic wall that is so rich, you feel like you can reach out and grab it.

This morning, I went to check out his website and saw that a new video and song has been posted on the front page. My first thought was that Devin Townsend was channeling his inner Steve Wilson (Porcupine Tree, Blackfield). It certainly feels that way for most of the track, called "Coast". However, towards the end, the music turns decidedly in the direction of Townsend's previous DevLab work; strangely ambient, white noise, feedback, and general audio chaos. What makes this track different though is that the audio chaos is neatly and almost gently rendered on top of the more steady musical background. It's really quite something to listen to.

Even better than the song though is the accompanying video. It's simply incredible; aliens, monoliths, all shot in grainy black and white to mimic hand-held VHS camcorders. It's very reminiscent of parts of the X-Files, or something from the mind of M. Night Shyamalan.

Check out the video, or visit the Devin Townsend website.


Crayon Physics

This has got to be one of the coolest games I have ever seen.

I've always been a fan of puzzle games, but in this particular game, the puzzle of how to connect a ball to a star separated by distances and objects, is as dynamic as your imagination.

Players are presented with more than 70 different puzzles and its brilliance is found in the way it challenges you to use your imagination to envision and then implement your own unique solutions to each one.

In the PC version, players use the mouse to draw shapes and objects that react to the laws of physics. For example, if you draw a box in the air, it will fall because of gravity.

What's particularly clever about this game (as you'll see in the video below) is that you can be as creative as you want. Simple levers and inclined planes work, but then again, so will rockets, dragons, and trebuchets.

Watch the video to see what I'm talking about... Other really interesting demonstrations can be seen here and here.

[tags]crayon, game, petri+purho, physics[/tags]

New Drupal Module Released

Back in October, I released my first module for Drupal, the open-source content management system. These days, I seem to be developing exclusively for Drupal, and with a robust API and thriving community, I can only say how much fun it is to work with.

However, like any platforms, there are pieces missing. Fortunately, Drupal is one of those platforms that is very easily extended through modules. I came across one of those missing pieces while working on a project. I do all of my development in a sandbox, and when the work is complete, I QA the product in a staging environment before pushing the code and database changes to a production environment. I found that there was no way to manage user/role permissions in code; all management was a manual process via the web interface.

Having to repeat manual tasks like this across different environments increases the odds of errors. Steps can be omitted or performed improperly. In the case of permissions, I decided to correct that by writing a module called Permissions API. What this module allows you to do is to grant and revoke permissions to roles in code.

This is probably most useful in the context of programmatically creating CCK content types. The ability to import CCK content types through code is great until you decide that you want members of specific roles to be able to do something with this content type. Currently, the only way to grant the permissions is to navigate through the access control page in the admin interface, which is completely unusable if you have a lot of roles and a lot of modules. This module addresses that problem by providing two functions:

permissions_grant_permissions()
permissions_revoke_permissions()

Each function accepts a role id and an array of permissions. Sample usage would be:

PHP:
  1. <?php
  2. function mymodule_update_1(){
  3.   // Handle roles and permissions
  4.   $rid = db_result(db_query('SELECT r.rid FROM {role} r WHERE r.name = \'some custom role\''));
  5.   if($rid> 0){
  6.     $permissions = array(
  7.       'create some_content_type content',
  8.       'edit some_content_type content',
  9.       'edit own some_content_type content',
  10.     );
  11.     permissions_grant_permissions($rid, $permissions)
  12.   }
  13. }
  14. ?>

This module also provides functions for granting all permissions defined by a given module to a given role, as well as granting all defined permissions to a given role, which is useful for creating admin-type roles. Sample usage would be:

PHP:
  1. <?php
  2. function mymodule_update_2(){
  3.   // Create a "super-admin" role by granting all permissions
  4.   $rid = db_result(db_query('SELECT r.rid FROM {role} r WHERE r.name = \'some custom role\''));
  5.   if($rid> 0){
  6.     permissions_grant_all_permissions($rid);
  7.   }
  8. }
  9. ?>

The next example shows how to grant all permissions defined by a specific module to a specific role:

PHP:
  1. <?php
  2. function mymodule_update_3(){
  3.   // Grant all permissions defined by a module's hook_perm implementation
  4.   $rid = db_result(db_query('SELECT r.rid FROM {role} r WHERE r.name = \'some custom role\''));
  5.   if($rid> 0){
  6.     permissions_grant_all_permissions_by_module($rid, 'some module')
  7.   }
  8. }
  9. ?>

[tags]api, code, drupal, module, permissions[/tags]

Drupal and Taxonomy Weights

I recently worked on a project in Drupal that called for a large number of taxonomy terms. I needed to put the terms in a specific order, but unfortunately, I had more terms than Drupal's weight field supports, which is a range from -10 to +10.

I did a quick search on Drupal, and was horrified to see how many people are hacking core to add a greater range. This is pretty easy to do without hacking core. All you need to do is create your own module that implements hook_form_alter().

my_module.module

PHP:
  1. function my_module_form_alter($form_id, &$form) {
  2.   switch($form_id) {
  3.     case 'taxonomy_form_term':
  4.     $form['weight']['#delta'] = 100;
  5.     break;
  6.   }
  7. }

And that's it!

[tags]alter, drupal, form, hook, module, taxonomy, term, weight[/tags]

Get Firefox Ad

This is a pretty cool ad for Firefox that just makes me laugh...

Firefox, IE, Opera, Netscape, browsers, funny, ad

Tradition vs. Technology

Raking leavesI've lived in New England my entire life, and if there's one thing that Yankees treasure, it's tradition. I'm no different, and yet today, I grudgingly decided that one tradition is not worth keeping.

Ever since I was a child, my family engaged in one of the most time-honored New England traditions of the autumn: raking leaves. Raking leaves is a noble chore. You get exercise and fresh air, your lawn benefits from being dethatched by the tines of the rake, and it instills a solid work ethic in the young.

It's a chore that I have always cherished. This year, however, I was rather dreading it. I just purchased a new home in September, and it has a substantially larger yard than my previous home. I knew that it would take me weeks to rake the entire property, and with a full time job, young kids, a wife, and everything else that keeps a family busy, I was trying to figure out how I could squeeze an extra 12 hours into each day.

Mostly out of curiosity, I decided to try a leaf blower. I'll categorically state that I find this to be cheating, but in the face of weeks of raking, I saw little reason to not give it a try. I'll have to admit, I was highly skeptical of the tool. As I mentioned before, one of the benefits of raking by hand is that the lawn gets a good dethatching; that is, all the dead grass and other material that forms a carpet right at the level of the soil and blocks nutrients from reaching the roots of the grass gets pulled up by the tines of the rake. I didn't see how a leaf blower would help with this.

At this point, I have to concede that the idea for the leaf blower was not mine. The leaf blower was a gift to me from my father, who is much wiser than I am. When he first gave it to me, I had no intention of using the thing for the reasons I had mentioned above. I have the unfortunate tendency to forget about things that I dismiss as useless, even if my determination regarding the usefulness of a thing is based on little or no prior experience. I don't know what it was about today that made me rethink my position on the leaf blower, but I was pleasantly surprised by its effectiveness.

As I mentioned, I have a large yard. Nearly two acres of level lawn, ringed by a thick woods of maple, oak, birch, and sumac. I was able to completely clear my backyard of fallen leaves in under an hour, a chore that would have easily taken me six hours had I resorted to my traditional techniques. The leaf blower was powerful enough to scrub the lawn and underlying soil clean. However excited I was about my progress and the ease of clearing the leaves, I could not help but feel sadness.

You see, I'm addicted to technology. I work with it every day, and run my life with it. The use of technology, even simple technology like a leaf blower, has implications. Using a leaf blower is a one-man job, and the tool is so loud, there's no chance for discussion and conversation while working. Furthermore, when you use a tool that cuts your work time down to next to nothing, the issue of work ethic is not even worth mentioning.

I was left wondering how I will teach all of these things to my sons. How will I teach them the value of sweat equity, the importance of physical labor and the satisfaction of putting your body to work with fantastic results? How can they learn the fine art of Yankee conversation, which occurs in short bursts, layered on top of physical work? The great New Hampshire author Noel Perrin described this very thing in one of his essays.

Sure, I can choose to abandon the leaf blower and go back to traditional methods. But as much as I have this choice, my Yankee sense of frugality forbids me from doing something that takes so much time when I have at my disposal the tools to get the job done faster leaving room for other things. Would I rather rake or spend the extra time with my sons?

As my father always reminds me, there's no such thing as setting aside quality time with your kids. He points out that any time you spend with your kids is quality. Perhaps I'll take this to heart and take my kids for a walk in the woods instead of raking up the woods.

[tags]autumn, chores, leaf+blower, leaves, new+england, new+hampshire, raking, work, yankee[/tags]

Flex and Drupal Paths

At CommonPlaces, each developer has his or her own sandbox to code in. Each sandbox can run n instances of a Drupal application, which all run out of subdirectories from the developer's web root. So, for example I have a structure like this:

  • /public_html
    • /drupal_site1
    • /drupal_site2

We have a similar staging environment for all of our QA, and then of course, we have the production servers.

When I develop Flex applications that need to connect to one of these instances, I run into fun with dynamic urls to the /services/amfphp gateway. Because Flex can interact with Javascript, it's trivial to build dynamic urls to whatever environment the Flex app is running in.

Step 1: Download and enable the JSTools module for Drupal. This module extends the Drupal javascript object and gives it extra properties.

Step 2: Construct your urls dynamically in Flex by utilizing the ExternalInterface object.


var protocol:String = ExternalInterface.call('location.protocol.toString');
var domain:String = ExternalInterface.call('location.hostname.toString');
var urlPath:String = ExternalInterface.call('eval', 'window.Drupal.settings.jstools.basePath');
var gatewayUrl:String = protocol + "//" + domain + urlPath + "services/amfphp";

Note that the protocol and domain strings are set via ExternalInterface calls to a javascript function. The urlPath string is set via a call to the eval() function, and I'm passing in an argument of the Drupal basepath property set by jstools.

Debugging this is also easy, if you use Firefox and the Firebug plugin. All you have to do is pass debug strings to the console:


ExternalInterface.call('console.info', 'protocol = ' + protocol);
ExternalInterface.call('console.info', 'domain = ' + domain);
ExternalInterface.call('console.info', 'path = ' + urlPath);
ExternalInterface.call('console.info', 'gateway url = ' + gatewayUrl);

With the dynamic url to your amfphp gateway set, you can be assured that your Flex client will be able to consume data from Drupal services, no matter where the Flex app is run.

[tags]actionscript, Adobe, AMFPHP, CommonPlaces, Drupal, Flex, javascript, jstools, module, Services, urls[/tags]

Hack-proof Your Drupal App – the Video

I had the pleasure of presenting at DrupalCon in Szeged Hungary, and the topic of my presentation was Drupal security from the perspective of the application. I am pleased to be able to share the video of my presentation.

Drupal, DrupalCon, CommonPlaces, Szeged, security, hacking, filters, output

DrupalCon Experiences in Szeged, Hungary

I have been attending DrupalCon this week, hosted in the beautiful Hungarian town of Szeged.

I was fortunate in that my company, CommonPlaces, was generous enough to become a silver sponsor for the conference. This gave me the opportunity to present a session on Drupal security, and a BoF session on cross-site request forgeries and mitigation strategies. The session on hack-proofing Drupal applications seems to have been well received; there was a mix of people in the audience in terms of skill levels and knowledge on the topic.

While the information I presented was well documented in various parts of drupal.org and other blogs, I think the practical demonstrations of attack strategies was eye-opening for many in the audience. There is a big difference, in my opinion, between knowing how to prevent a vulnerability and knowing the mechanics and practical application of a vulnerability. The practical demonstrations were handled by Arian Evans from WhiteHat Security, as my co-presenter.

There was a wide variety of sessions offered at DrupalCon, and one of my favorites by far was on the topic of attracting and retaining Drupal talent. This was a very candor look at how some of the larger Drupal shops (RainCity, Palantir, and Development Seed) run their businesses and profit from working with Drupal.

The huge presence of Acquia here at DrupalCon is very exciting, and I'm very excited to see what they are up to.

If you haven't gotten the chance to attend a DrupalCon before, I hope that you find a way to beg, borrow, or hitchhike your way to the next one.

DrupalCon, CommonPlaces, Drupal, security, sessions

Return top

INFORMATION

Drupal development since v4.7