Sticky MPKNOWLEDGEDROP

Track Interactions on (iFrame embedded) Youtube Videos

  • 10 February 2020
  • 0 replies
  • 368 views
Track Interactions on (iFrame embedded) Youtube Videos
Userlevel 1
  • Mixpanel Community Team
  • 1 reply

Every week, we will release tips to help you get the most out of Mixpanel. Want to see more? Click here to see other #mpknowledgedrop articles.

 

Table of Contents

  • Introduction

  • Game Plan

  • Building the Piping

  • Building the Logic

  • TLDR, take me to the code!

 

Introduction

 

Normally, when you embed content via an <iframe> on your website that fetches a resource on an external domain, the parent page (your page) doesn’t have access to execute scripts inside the child <iframe>… this can make it difficult to track user actions on the child <iframe> and has caused a few headaches when the use of embedded content (like a video, song, or media object) is an important of your data. “Did they watch the video?”, "How long did they make it?” … that stuff is important!

Luckily, YouTube provides an <iframe> API to us; this means we can write javascript on our parent page which is aware of ‘video player’ events that occur inside the embedded child iframe. The only requirement is that the end-user’s browser supports HTML5’s postMessage feature - which (at the time of writing) is supported by ALL major browsers.

So, given an  <iframe>  embedded media object like:

<iframe id="myPlayer" src="https://www.youtube.com/embed/p3G5IXn0K7A?enablejsapi=1"> </iframe>

We can add (example 1) code to our parent page:

var tag = document.createElement('script');

tag.id = 'iframe-demo';

tag.src = 'https://www.youtube.com/iframe_api';

var firstScriptTag = document.getElementsByTagName('script')[0];

firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);

var player;

And now, we can define our own handlers to run as the player moves through its different lifecycles:

function onYouTubeIframeAPIReady() {

    player = new YT.Player('myPlayer', {
        events: {
            'onReady': onPlayerReady,
            'onStateChange': onPlayerStateChange
        }
    });

}

In this case onPlayerReady() and onPlayerStateChange() are functions we define and control, which will allow us to granularly track in Mixpanel how each user interacts with the embedded video player.

 

Note that the binding - new YT.Player(‘myPlayer’) - requires us to add an ID like ‘myPlayer’ to our <iframe>, so our javascript code knows which embedded object to reference. We also need the query string parameter ?enablejsapi=1 in the iFrame’s src attribute, which enables the iFrame API for the player.

 

GAMEPLAN

Before we start writing our own code, it’s often helpful to draw a picture of our desired state. In this implementation of video tracking, our goal looks something like this:

tgxdQ84_eANOp9lEtu-aLRcKYUOROvXGgC4rITFcxhKLxN-CKLQUWDQtanzs2rchJuroPTVHqIB8GnFoNBJqPlKHStQtPwxb0BUHekkNwCTUHEd5O7iOpwtJYn0poRIfOUNfMOEu

Essentially, we want to:

  • Start a timer, when the page loads

  • Stop the timer when the player loads and track an event

  • Start a new timer, waiting for the video to buffer

  • Stop the timer when the video buffers (and playback starts)

  • Start a new timer, waiting for the video to complete

  • Track ‘play/pause’ actions (or any other ‘player’ things we care about)

  • Stop the time when the video ends (or the user leaves) and track the event

 

Each event should contain (at least) the video’s name, playback quality, length, author, and any other meta-attributes which describe the video (that we care about).

Ok, that’s our picture and our game plan... let’s start coding:

 

BUILDING THE PIPING

First we need to initialize Mixpanel with our project token; I’m also going to turn Mixpanel’s debugging on (so Mixpanel events output in the console). Finally, we’ll setup a timer, to properly time how long it takes the player to load, after the page is ready:

mixpanel.init("your-project-token", {
    debug: true,
    loaded: function(mixpanel) {
        mixpanel.time_event('player loaded!');
    }

});

Then we need to make sure that we can easily reference the embedded iFrame video, which I’m calling ‘myPlayer’using the following snippet:

var tag = document.createElement('script');

tag.id = 'iframe-demo';

tag.src = 'https://www.youtube.com/iframe_api';

var firstScriptTag = document.getElementsByTagName('script')[0];

firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);

var player;

Next, we’ll make a small utility function that’s designed to grab some information (properties) about the video itself and return an object that can be passed to Mixpanel via .track():

function getVideoInfo() {

    //get some information about the video

    var videoInfo = player.getVideoData();

    var videoProps = {
        'video quality': player.getPlaybackQuality(),
        'video length': player.getDuration(),
        'video url': player.getVideoUrl(),
        'video title': videoInfo.title,
        'video id': videoInfo.video_id,
        'video author': videoInfo.video_id
    }

    return videoProps;

}

This is not an exhaustive list of the methods provided to us by the YouTube player, but you can find one here

Next, we’ll need to create function that instructs the YouTube API to call our lifecycle methods when the player ‘changes states’:

function onYouTubeIframeAPIReady() {

    player = new YT.Player('myPlayer', {
        events: {
            'onReady': onPlayerReady,
            'onStateChange': onPlayerStateChange
        }

    });

}

Note that the name of this function -onYouTubeIframeAPIReady() - needs to be named as such; it is not called by our code directly, but rather called by the YouTube iFrame API as per: https://developers.google.com/youtube/iframe_api_reference#july-19,-2012

If we had multiple videos on a single page, we would name them accordingly (using their ID’s to delineate them) and call new YT.Player() for each one, with the appropriate event bindings for onPlayerReady() and onPlayerStateChange()

 

BUILDING THE LOGIC

Finally, we need to define the tracking logic for onPlayerReady() and onPlayerStateChange().

First let’s define onPlayerReady():

function onPlayerReady(event) {

//at this point the 'player' has finished loading
    var videoInfo = getVideoInfo();
    mixpanel.track('player loaded!', videoInfo);

//start the 'video started' timer
    mixpanel.time_event('video started!'); 

}

To make sure this is working, we’ll load our page in our browser, and we should see a ‘player loaded’ event with a ‘$duration’ property that represents the amount of time that elapsed from page initial page load to the player being loaded! (take a look at the picture below)

eIZs4WA_CZcDwSqKLfdVLPV1D_qv7JmCFAo_dSBdjLxY6piUkY3vVGqZA12Lj0UY2Ljz8DPPVIk1xTWj2y0YLKID4I9nnTPwuCicVnNWy5llHdSb05h24XuzCb5HBD7ppEtrd6te

Rather than writing all of our business logic in onPlayerStateChange() we’re going to use delegation and split this up into two functions - onPlayerStateChange() will delegate its work to trackPlayerChanges()

Here’s how I would write both of those functions; I’ve added helpful comments along the way for reference:

function onPlayerStateChange(event) {
    trackPlayerChanges(event.data);
}

function trackPlayerChanges(playerStatus) {
    var videoInfo = getVideoInfo();

    if (playerStatus == -1) {
//playback hasn't started, but the player is loaded
        mixpanel.track('video started!', videoInfo);
//start the 'video buffered' timer
        mixpanel.time_event('video buffered!'); 
    } else if (playerStatus == 0) {
     //playback has ended
        mixpanel.track('video finish!', videoInfo);
    } else if (playerStatus == 1) {
     //video is playing
        mixpanel.track('playback started!', videoInfo);
//re-start the 'video buffered' timer
        mixpanel.time_event('video buffered!'); 
//start the 'video finish!' timer
        mixpanel.time_event('video finish!'); 
    } else if (playerStatus == 2) {
      //video is paused, get the current time it’s paused at
        videoInfo.$duration = player.getCurrentTime(); 
        mixpanel.track('playback paused!', videoInfo);
    } else if (playerStatus == 3) {
        // video is buffering
        mixpanel.track('video buffered!', videoInfo);
    } else if (playerStatus == 5) {
        // video is cued; loaded but not playing
        // we don't really care about this

    }

}

(for more info on the ‘player statuses’ and what they mean, consult: https://developers.google.com/youtube/iframe_api_reference#Playback_status)

Pop-open the developer console, and you can see the events sent to Mixpanel in real-time! Don’t forget to turn the ‘debug’ flag to ‘false’ in mixpanel.init() before shipping this code to production!

 

VERIFYING THAT IT WORKS

To verify our work, we can open up Mixpanel’s ‘Live View’ and see that we’re getting all the metadata and time elapsed for all of our events!


QzEwtUgcR3NdWErF5876-zDbLINjctSfJpvq2FBjKBueKEikIGSw0z7wPvF553v8xb0-K-l9bF28xzWfcETjYurcImfsd2BwAhPpstiiUY2BgK42ejzmebVt5DmLcCsfs_Dpe4Na
 

TLDR, take me to the code!

If you'd rather just copy/paste the code and figure out how it works later... here's the whole script in its entirety:

Given and iFrame of the form:

<iframe id="myPlayer" src="https://www.youtube.com/embed/p3G5IXn0K7A?enablejsapi=1"></iframe>

Our Javascript is:

mixpanel.init("your-project-token", {
    debug: true, //CHANGE THIS TO FALSE FOR PRODUCTION!
    loaded: function(mixpanel) {
        console.log('mixpanel loaded!');
        //start the 'player loaded' timer
        //https://developer.mixpanel.com/docs/javascript-full-api-reference#section-mixpanel-time_event 
        mixpanel.time_event('player loaded!');
    }
});

//required to fetch the youtube iFrame API
var tag = document.createElement('script');
tag.id = 'iframe-demo';
tag.src = 'https://www.youtube.com/iframe_api';
var firstScriptTag = document.getElementsByTagName('script')[0];
firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
var player;

// lifecycle methods for the player
function onYouTubeIframeAPIReady() {
    player = new YT.Player('myPlayer', {
        events: {
            'onReady': onPlayerReady,
            'onStateChange': onPlayerStateChange
        }
    });
}

function getVideoInfo() {
    //get some information about the video
    var videoInfo = player.getVideoData();
    var videoProps = {
        'video quality': player.getPlaybackQuality(),
        'video length': player.getDuration(),
        'video url': player.getVideoUrl(),
        'video title': videoInfo.title,
        'video id': videoInfo.video_id,
        'video author': videoInfo.video_id

    }
    return videoProps;
}

function onPlayerReady(event) {
    //at this point the 'player' has finished loading
    var videoInfo = getVideoInfo();
    mixpanel.track('player loaded!', videoInfo);
    mixpanel.time_event('video started!'); //start the 'video started' timer
}


function onPlayerStateChange(event) {
    trackPlayerChanges(event.data);
}

//player states: https://developers.google.com/youtube/iframe_api_reference#Playback_status
function trackPlayerChanges(playerStatus) {
    var videoInfo = getVideoInfo();

    if (playerStatus == -1) {
        //playback hasn't started, but the player is loaded
        mixpanel.track('video started!', videoInfo);
        mixpanel.time_event('video buffered!'); //start the 'video buffered' timer
    } else if (playerStatus == 0) {
        // playback has ended
        mixpanel.track('video finish!', videoInfo);
    } else if (playerStatus == 1) {
        // video is playing
        mixpanel.track('playback started!', videoInfo);
        mixpanel.time_event('video buffered!'); //re-start the 'video buffered' timer
        mixpanel.time_event('video finish!'); //start the 'video finish!' timer
    } else if (playerStatus == 2) {
        // video is paused
        videoInfo.$duration = player.getCurrentTime(); //get the current time when the video is paused
        mixpanel.track('playback paused!', videoInfo);
    } else if (playerStatus == 3) {
        // video is buffering
        mixpanel.track('video buffered!', videoInfo);
    } else if (playerStatus == 5) {
        // video is cued; loaded but not playing
        // we don't really care about this
    }
}

 

Have fun out there!

AK


0 replies

Be the first to reply!

Reply