MediaElement.js with Google Tag Manager | GTM tracking MediaElement

MediaElement.js is one of the most popular video players around especially with WordPress websites.  Check out the latest player statistics here at Built With.

Tracking your video player usage can give you great insight into the videos you are hosting - what topics, creators, length ... of videos people watch.  And how these video views relate to your organization's overall website goals.

Before we begin - credit where it is due - David Vallejo at Thyngster wrote an agnostic tracker for JWPlayer.  We are going to use this same agnostic approach to our MediaElement.js tracking.  The beauty of this is that the Tags and Triggers will work for either player.  (For example, when you have a client using both players on a website - or maybe three players!)

HTML Code We based this code on David's code for the dataLayer updates and the MediaElement.js API for the events.  Here is the complete code below - and then we'll step through each part.

<script>
(function(){
var i = 0;
// Define at which percentages you want to fire an event
var markers = [25,50,75,90];
var playersMarkers = [];

function findObjectIndexById(haystack, key, needle) {
    for (var i = 0; i < haystack.length; i++) {
        if (haystack[i][key] == needle) {
            return i;
        }
    }
    return null;
}

while (true) {
    var mediaElement = document.getElementsByTagName('video')[i];
    if (!mediaElement.player.id)
        break;

    playersMarkers.push({
      'id': mediaElement.player.id,
      'markers': []
    }); 

    mediaElement.addEventListener('play',
        function(e){
            dataLayer.push({
                "event": "video",
                "player_id": this.player.id,
                "interaction": "Play",
                "video_url": this.getSrc(),
                "duration": this.getDuration(),
                "width": this.width,
                "height": this.height,
                "position": this.getCurrentTime(),
                "resolutions": 'not set', // sadly not available is MEJS
                "volume": this.getVolume(),
                "player_type": 'not set'
            });   
        }, false);

    mediaElement.addEventListener('ended',
      function(e){
            dataLayer.push({
                "event": "video",
                "player_id": this.player.id,
                "interaction": "Complete",
                "video_url": this.getSrc(),
                "duration": this.getDuration(),
                "width": this.width,
                "height": this.height,
                "position": this.getCurrentTime(),
                "resolutions": 'not set', // sadly not available is MEJS
                "volume": this.getVolume(),
                "player_type": 'not set'
            });   
      }, false);

    mediaElement.addEventListener('pause',
      function(e){
            dataLayer.push({
                "event": "video",
                "player_id": this.player.id,
                "interaction": "Pause",
                "video_url": this.getSrc(),
                "duration": this.getDuration(),
                "width": this.width,
                "height": this.height,
                "position": this.getCurrentTime(),
                "resolutions": 'not set', // sadly not available is MEJS
                "volume": this.getVolume(),
                "player_type": 'not set'
            });   
      }, false);

    mediaElement.addEventListener('volumechange',
      function(e){
            dataLayer.push({
                "event": "video",
                "player_id": this.player.id,
                "interaction": "Volume Change",
                "video_url": this.getSrc(),
                "duration": this.getDuration(),
                "width": this.width,
                "height": this.height,
                "position": this.getCurrentTime(),
                "resolutions": 'not set', // sadly not available is MEJS
                "volume": this.getVolume(),
                "player_type": 'not set'
            });   
      }, false);

    mediaElement.addEventListener('timeupdate',
      function(e){
        var percentPlayed = Math.floor(this.getCurrentTime()*100/this.getDuration());
        var playerMarkerIndex = findObjectIndexById(playersMarkers,'id',this.player.id);

        if(markers.indexOf(percentPlayed)>-1 && playersMarkers[playerMarkerIndex].markers.indexOf(percentPlayed)==-1)
        {          
            playersMarkers[playerMarkerIndex].markers.push(percentPlayed);
            dataLayer.push({
                "event": "video",
                "player_id": this.player.id,
                "interaction": "Progress " + percentPlayed + "%",
                "video_url": this.getSrc(),
                "duration": this.getDuration(),
                "width": this.width,
                "height": this.height,
                "position": this.getCurrentTime(),
                "resolutions": 'not set', // sadly not available is MEJS
                "volume": this.getVolume(),
                "player_type": 'not set'
            });             
        }       
      },
    false);  
    i++;
}    
})();
</script>

The initial lines of this JavaScript is used to initialize the Media Element Player(s) on the page and the Percent Progress updated in the dataLayer.

var i = 0;
// Define at which percentages you want to fire an event
var markers = [25,50,75,90];
var playersMarkers = [];

function findObjectIndexById(haystack, key, needle) {
    for (var i = 0; i < haystack.length; i++) {
        if (haystack[i][key] == needle) {
            return i;
        }
    }
    return null;
}

Let's run down how each variable / function is used in the code:

i is used to scroll through each video Media Element player on the page. markers - sets the percentage progress to set the dataLayer.  To add new or remove existing percentages edit this variable. playersMarkers - this is an array of objects that stores the player's id and percentage that has been set.   findObjectIndexById - is a function used to search for the index of a key using the needle input.  It is used to maintain the playersMarkers array for each player and the percent progress (markers) set in the dataLayer. Now for the start of the 'while' loop.

while (true) {
    var mediaElement = document.getElementsByTagName('video')[i];
    if (!mediaElement.player.id)
        break;

    playersMarkers.push({
      'id': mediaElement.player.id,
      'markers': []
    }); 

Well it starts with a 'while(true)' so we are going to continue to loop through this code while this script is loaded.

 mediaElement is our current 'video' element - it is where we will access the player and all the player values via the MediaElement API.  If you wanted to track MediaElement Audio - simply change the 'video' to 'audio' and the dataLayer events to 'audio' and you are now tracking audio files as well.  (Sure someone out there will figure out a good way to do this in a single file!) Next in the if statement we check to see if the mediaElement player has been initialized - if not we break out of the code. playersMarkers.push - adds the player(s) id to the array and initializes the markers for that id with an empty array. The next parts of the code are blocks for the MediaElement API events - each one looks like this:

<script>
    mediaElement.addEventListener('play',
        function(e){
           // do some good stuff here
        }, false);

We will add listeners for each event we want to track - this is for the 'play' event.  This code will track for each of these events:

  • Play - when ever someone clicks on the play button.  (The position (currentTime) is also sent back so you can differentiate the first 'play' with position = 0 from a play at other points.
  • Ended - when the player reaches the end.
  • Pause - when the user clicks on the player's pause button.
  • Volume Change - when the user raises or lowers the volume for the video player.
  • Time Update - this is the event the code checks for the progress and sets the dataLayer when we reach a progress point in our markers array. Now let's break down the dataLayer push:
dataLayer.push({
   "event": "video",
   "player_id": this.player.id,
   "interaction": "Volume Change",
   "video_url": this.getSrc(),
   "duration": this.getDuration(),
   "width": this.width,
   "height": this.height,
   "position": this.getCurrentTime(),
   "resolutions": 'not set', // sadly not available is MEJS
   "volume": this.getVolume(),
   "player_type": 'not set'
});   

Again we are using the agnostic dataLayer updates from David's JWPlayer GTM Tracker post so send back the same variables - with values from the MediaElement player.  You will see that the MediaElementPlayer does not support all the data available in the JWPlayer - so we have put in everybody's not favorite 'not set' value.  They are:

  • event - we use video for all updates - we can then create a custom event trigger to fire on 'video'.  (Again a tracker for an Audio player could have an 'audio' event value. 
  • player_id - the id for that player.  (There can be multiple players on a page.)
  • interaction - the interaction for the cooresponding MediaElement event - so play for play, pause for pause, progress % for timeupdate...
  • video_url - the URL where the video is hosted.  (You may want to write a bit of JavaScript to remove the query string parameters prior to reporting.)
  • duration - the time in seconds to watch the full video.
  • width - width of the player in pixels.
  • height - height of the player in pixels.
  • position - the current position of the player in seconds when the dataLayer is updated.
  • resolutions - not available in the MediaElement player API.  (Or I could not find it - suggestions welcome!)
  • volume - a value between 1 and 0 for the current volume when the dataLayer is updated.
  • player_type - same as resolutions not available in the MediaElement player API.  (Or I could not find it - suggestions welcome!) There is quite a bit more information you could included in this dataLayer update from the MediaElement API - so feel free to add your favorite variable(s) as well.

A quick note on the code in the timeupdate event:

        var percentPlayed = Math.floor(this.getCurrentTime()*100/this.getDuration());
        var playerMarkerIndex = findObjectIndexById(playersMarkers,'id',this.player.id);

        if(markers.indexOf(percentPlayed)>-1 && playersMarkers[playerMarkerIndex].markers.indexOf(percentPlayed)==-1)
        {          
            playersMarkers[playerMarkerIndex].markers.push(percentPlayed);

The first line calculates the percent played using the video Duration and CurrentTime. Then it uses the findObjectIndexById to check to see if we have recorded a dataLayer event for this player and percentage combination. If not our statement is true in the if statement. And then we update the playersMarkers for that player ID - playerMarkerIndex with the percentPlayed. This nifty logic ensures we only send back a percentPlayed once for each marker and each player.

GTM Sample Implementation

This dataLayer update is agnostic so can be used for a Google Analytics event, Conversions (say on 90% progress...), you name it.  To wrap up this post I will include a sample Google Analytics Event with the Variables, Trigger and Tag.

Variables

All the data is in the dataLayer so really simple series of dataLayer variables:

GTM-MediaElementTracker-Position-DataLayer

GTM-MediaElementTracker-VideoStatus-DataLayer

GTM-MediaElementTracker-VideoURL-DataLayer

Data Layer Variables with the exact names from above... So pretty easy! 

And to stick with the easy theme - our Trigger:

GTM-MediaElementTracker-Video-Trigger

and then we can send this back to Google Analytics with this tag:

GTM-MediaElementTracker-GAVideoTrackingTag

In this example we are removing the Query String parameters from our Video URL using this simple JavaScript Variable:

GTM-MediaElementTracker-URLNoQueryString-DataLayer

Summary

Hope you found this useful and that it also gives you ideas on how to extend this or other useful scripts out on the web!  Would love to hear from you about how you used this and any future topics you would like to see.

All the Best!


0 Comments: