by Doug

14/06/2013

Internet Marketing

Tracking YouTube HTML5 Players with Google Analytics

Tracking YouTube Video Players

Video is great. We like to write about our products and services on our websites as do our clients but in the same way as a picture is worth a thousand words, a video can be worth much more. YouTube gets over 1 Billion unique visitors a month (http://www.youtube.com/yt/press/statistics.html). We know users and customers watch the videos on our sites but we need to properly quantify the amount of video consumption in order to relate user activity to the return economic from videos. To do this we need to track YouTube player usage.

Back story

Way back in the past, a range of super clever scripts were built to tap into the YouTube Flash Player javascript API. These scripts were used by a large number of people to track YouTube (YT) player usage on websites. I was one of those people. As is the way with many #measure folks, we tinkered with the scripts. I tinkered and extended a script. My clients used the script and we all saw a lot of value from tracking YT video consumption.

Why HTML5?

Many devices (tablets and smart phones of certain brands) do not support Flash. Depending on your point of view and tolerence of the technology you may choose to think this is a good or bad thing – up to you. The fact is that users with these brands of devices can’t or couldn’t see YouTube videos. Now, YouTube offers an HTML5 player, these users can now be exposed to great video content.

The old tracking script is not usable. The new HTML5 player needs a new tracker. This is it.  It’s a little rough.  If you use it test it first! I’m still refining the script so don’t smack me down for some rough looking coding – it’ll get there.  I’m happy to receive feedback on this if you have suggestions for improvements.  With the caveats out of the way we can get digging:

YouTube HTML5 Tracking with Google Analytics

Here’s a simple demo – http://tagify.co.uk/ythtml5/ythtml5.html

Use Live HTTP Headers, Charles, Fiddler, Firebug or whatever your GA debugging weapon of choice is and you’ll see these events firing:

  • Playing (buckets from 0-25%, 25-50%, 50-75% and 75-100%)
  • Paused
  • Ended
  • Rewind
  • Fast forward

There are more events available and are fully documented here – https://developers.google.com/youtube/iframe_api_reference

Detail

Let’s deconstruct this example and look at it in more detail:

iFrame embed

We’re embedding the HTML player in an iFrame:

<iframe id="tagify_video_html52" type="text/html" width="640" height="390"
 src="http://www.youtube.com/embed/iRsV6YpLsKA?enablejsapi=1&origin=http://tagify.co.uk"
 frameborder="0"></iframe>

I’ve signed up to the HTML5 trial – http://www.youtube.com/html5?gl=GB You’ll need to do this on your desktop machine or you’ll see the flash player and this demo and post will suck…

Notice the portion of script above in bold.  Make sure you correctly set your origin.

iFrame API

We need to load the iFrame API:

(function() {
 var ytscript = document.createElement('script'); ytscript.type = 'text/javascript';
 ytscript.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.youtube.com/iframe_api';
 var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ytscript, s);
 })();

Startup

We’ll instantiate a couple of variables, one to represent the player and an empty array to hold all our players.  The latter enables multiple players to be tracked at once if need be:

var player;
 var playerContainers = [];

Then, we wait…

function onYouTubeIframeAPIReady() {

The players will call this method when they’re good and ready to play…

We use a little slice of jQuery to find all the iFrames with a source containing youtube.  The playerContainers array is populated with these objects.  Yes, the array will get rebuilt every time the onYouTubeIframeAPIReady method is called which is a little wasteful – it’s one of those refactoring things I mentioned earlier that I haven’t got around to yet…

playerContainers = jQuery('iframe[src*="youtube"]');

Next, we’ll loop through the playerContainers, pull out the ID of the iFrame, build a trkr object (explained in a moment) and populate the vdtrkr5.trkrs array with this object.

for(v=0;v<playerContainers.size();v++){
 playerId = jQuery(playerContainers[v]).attr('id');
 vdtrkr5.trkrs[playerId] = new trkr(playerId,'',[]);

Now we get a little funky.  We need two methods constructing that are ‘owned’ by each player – _onPlayerStateChange and _onPlayerReady.  We use eval (funky!) to build these methods:

eval(playerId + "_onPlayerStateChange = function(event){vdtrkr5.trkrs['"+playerId+"']._onPlayerStateChange(event);}");
eval(playerId + "_onPlayerReady = function(event){vdtrkr5.trkrs['"+playerId+"']._onPlayerReady(event);}");

Now we get back to the YT API and tell the player that the methods we’ve just built should be used when onReady and onStateChange fire:

player = new YT.Player(playerId, {
 events: {
   'onReady': playerId+"_onPlayerReady",
   'onStateChange': playerId+"_onPlayerStateChange"
 }
});

Now we drop the reference to the YT player object in our vdtrkr5.trkrs array for use later on:

 vdtrkr5.trkrs[playerId].YTPlayer = player;

Now we have our players registered.  We have an array of player objects and the players know that when the Ready and StateChange events fire, the players need to call our methods so we can process the events.

The trkr object

The trkr object contains a bunch of variables that are used when processing player events:

 this.playerId = id;
 this.videoTitle = title == '' ? '' : title;
 this.actions = actions;
 this.lastAction = '';
 this.lastPosition = 0;
 this.lastBucket = -1;
 this.pausedAt = 0;
 this.videoDuration = 0;
 this.YTPlayer = {};

The first method likely to be called will be _onPlayerReady.  Remember we set this up using the eval earlier.  This method grabs the video title and then sets up a setInterval to check the playing status of the player once a second (you can change this interval but I find once a second is fine):

this._onPlayerReady = function(event){
  this._getVideoTitle();
  if(typeof(playersTracker)=='undefined' && typeof(vdtrkr5.trkrs)!=undefined && !jQuery.isEmptyObject(vdtrkr5.trkrs)){
    var playersTracker = setInterval("_Playing()",1000);
  }
}

Getting the video title is a nice to have. If you record the video title in the markup, you won’t need to do this but it’s kinda neat to know how to do anyway:

this._getVideoTitle = function(){
  var videoIdRegEx = /.*v=(.*)&.*/i;
  videoId = this.YTPlayer.getVideoUrl().match(videoIdRegEx)[1];
  player = this;
  $.ajax({
    url: 'https://gdata.youtube.com/feeds/api/videos/' + videoId + '?alt=json',
    type: 'GET',
    dataType: 'jsonp',
    success: function(root) {
      player.videoTitle = root.entry.title['$t'];
    }
  });
}

So, we pull the video ID out of the video URL using a regex on the returned value from a call to the YTPlayer.getVideoUrl() method.  Whe then make an AJAX call to YouTube to get the video detail which contains the title.  Job done.

Now we can handle the events that fire when the player state changes:

this._onPlayerStateChange = function(event) {
 thePlayer = this;
 playerPos = parseInt(thePlayer.YTPlayer.getCurrentTime(),10);
 playerPercent = parseInt((playerPos/thePlayer.YTPlayer.getDuration())*100);
 switch(event.data) {
   case YT.PlayerState.ENDED:
     thePlayer.actions.push({actionname: 'ended'});
   break;

   case YT.PlayerState.PAUSED:
     thePlayer.actions.push({actionname: 'paused', mainval: playerPercent});
   break;
 }
 if(thePlayer.videoTitle != '' && thePlayer.actions.length > 0){
   $.each(thePlayer.actions, function() {
     if(thePlayer.lastAction.actionname != thePlayer.actions[0].actionname){
       thePlayer._trackVideo(thePlayer.actions[0].actionname, thePlayer.videoTitle, thePlayer.actions[0].mainval, thePlayer.actions[0].percent);
     }
     thePlayer.lastAction = thePlayer.actions.shift();
   });
 }
}

We check to see where the player positions is – how much video has been seen – using YTPlayer.getCurrentTime().  Some maths tells us this value as a percentage of the overall video duration – YTPlayer.getDuration().

The small switch only handles ended and paused events – as previously mentioned, there are more events available if you need to record them.  Events can fire quickly and often.  We fire the GA events asynchronously so the event detail is pushed onto the trkr.actions array for processing later.

Remember the setInterval we setup to fire once a second earlier?  Let’s deal with that now.  This is one of the busiest parts of the script.  It handles the playing status so it’s quite important. This version checks to see which quarter of the video the user has watched through to.  You may want to change this to record the seconds of play or drop this in 10 second time buckets.  Your requirements will depend on the typical length of your videos.

First, we’ll set up our quarterly time buckets:

_Playing = function(){
  var self = vdtrkr5,
    portions = [0,25,50,75,100];

Now, we’ll loop through our players and see what state they’re in comparing getPlayerState with YT.PlayerState.PLAYING:

    $.each(vdtrkr5.trkrs, function(i,trkr){    
      thePlayer = vdtrkr5.trkrs[i];   
      var lower_bucket = 0,
      upper_bucket = 0;       

    try{
      if(thePlayer.YTPlayer.getPlayerState()==YT.PlayerState.PLAYING){

Now we know the player is playing we’ll figure out where it has played to in terms of seconds and percentage duration:

        thePlayer.videoDuration = thePlayer.videoDuration==0 ? thePlayer.YTPlayer.getDuration() : thePlayer.videoDuration;

        playerPos = parseInt(thePlayer.YTPlayer.getCurrentTime(),10);                    
        playerPercent = (playerPos/thePlayer.videoDuration)*100;

The maths below decides if the player has moved on enough to sit in a new time bucket.  If it has we can push an action on to the actions array.  If the player position has changed by more than 10 seconds we can safely assume a forward or rewind action has taken place:

          if(playerPos>thePlayer.lastPosition-10 && playerPos<thePlayer.lastPosition+10){
            //normal playing - how far?
            for(p=0;p<4;p++){
              lower_bucket = playerPercent==portions[p]||playerPercent>portions[p]&&playerPercent<portions[p+1] ? portions[p] : lower_bucket;
              upper_bucket = playerPercent==portions[p]||playerPercent>portions[p]&&playerPercent<portions[p+1] ? portions[p+1] : upper_bucket;
            }

            if(lower_bucket!=thePlayer.lastBucket){
              thePlayer.actions.push({actionname: 'playing', mainval: lower_bucket + '-' + upper_bucket, optvalue: playerPos, percent: playerPercent});
              thePlayer.lastBucket = lower_bucket;        
            }
          }else{
            //rewind or forward-wind?
            if(playerPos<thePlayer.lastPosition-10){thePlayer.actions.push({actionname: 'rewind'})};
            if(playerPos>thePlayer.lastPosition+10){thePlayer.actions.push({actionname: 'fast-forward'})};
          }

Okay, we’ve figured out if the player was playing, rewound or forward wound and by how much so now we remember where the player position was at the last point of inspection ready for comparison next time:

        thePlayer.lastPosition = playerPos;

Now, if the video has a title and has actions we know we have to track something so let’s rifle through the actions and ask _trackVideo to take care of the GA events for us:

     if(thePlayer.videoTitle != '' && thePlayer.actions.length > 0){                      
          $.each(thePlayer.actions, function() {                  
            thePlayer._trackVideo(thePlayer.actions[0].actionname, thePlayer.videoTitle, thePlayer.actions[0].mainval, thePlayer.actions[0].percent)
            thePlayer.lastAction = thePlayer.actions.shift();
          });
        }
      }
    }catch(err){}  
  });
}

Processing Actions

The _trackVideo method is essentially a wrapper around _trackEvent with a switch/case statement to decide which type of event to fire:

this._trackVideo = function(action, video, val1, val2){
  var self = vdtrkr5;
  switch (action) {
    case 'playing':
      if(typeof(val1)!='undefined' && typeof(val2)!='undefined'){
        _gaq.push(['_trackEvent', 'Video - HTML5','Playing - ' + val1 + '% ',video, parseInt(val2)]);
      }else{
        _gaq.push(['_trackEvent', 'Video - HTML5','Playing - ' + val1 + '% ',video]);
      }     
      break;
      case 'error':
        _gaq.push(['_trackEvent', 'Video - HTML5','Error - ' + val1,video]);
      break;
      default:
        if(typeof(val1)!='undefined'){
          _gaq.push(['_trackEvent', 'Video - HTML5',action,video, parseInt(val1)]);
        }else{
          _gaq.push(['_trackEvent', 'Video - HTML5',action,video]);
        }
    }
  }
}

Wrap up

That’s all I have to say about the code – warts and all!  So, you can track YouTube HTML5 video players.  You can handle multiple players, get the title, see when play, pause, rewind, forward-wind and ended events occur and when they occurred.

These events are recorded in GA to help you determine the ROI on videos.

I hope you find this useful.  I’ll post updates as I refine the script.

Comments

Leave a comment

Your email address will not be published. Required fields are marked *