Contact Us Support Forum Get Email Updates
 
 

Thanks! Someone will be in touch with you shortly.

Rather just email us? Email us here.
Rather speak with someone in person?
Call any time with Experience API questions:

866.497.2676

YouTube/xAPI Tech Tips

Posted by

Categories: Best Practices, Ideas, Statements, xAPI

Posted 28 August 2013

 

A lot can be done with xAPI and videos, and this post is about how I overcame some technical hurdles with YouTube’s API, specifically.

First of all, for basic setup of the YouTube player we follow the instructions here. We will need to include swfobject.js to allow cross-domain iframe calls, and create an iframe in which to house the player. I chose to follow the documentation pretty closely on this one.

Next, we will need to make a call to swfobject.embedSWF(). Note here that we will pass in a few parameters and attributes like params = { allowScriptAccess: “always” };. This is what my function looks like. Note that I pass in the id (ytapiplayer) where I want the player located.

swfobject.embedSWF("http://www.youtube.com/v/" + 
videoID + //initial video to load "?enablejsapi=1&playerapiid=ytplayer&version=3",
        		"ytapiplayer", 
"800", "500", //initial width and height of player 
"8",null, null, params, atts);

 
Next, we will want to track when this player changes state (loaded, playing, paused, finished, etc). We should at the very least have functions in our JavaScript named onYouTubePlayerReady and onPlayerStateChange. The first is called when the player is loaded. In this function, we need to add the state change event handler.

function onYouTubePlayerReady(playerId) {
    ytplayer = document.getElementById("myytplayer");
    ytplayer.addEventListener("onStateChange","onPlayerStateChange");
    //player loaded
    //do some stuff here if you want!    
}

 
The second function will be called every time the YouTube player changes state, and in turn these states can be used to track specific interactions and events. My onPlayerStateChange looks very simple but it’s able to capture some pretty important data.

function onPlayerStateChange(newState) {
    switch (newState) {
    	case (YT.PlayerState.PLAYING):
        videoStarted();
        break;
    	case (YT.PlayerState.PAUSED):
        if (lastPlayerState == YT.PlayerState.PLAYING) {
            videoWatched(lastPlayerTime, ytplayer.getCurrentTime())
        } else if (lastPlayerState == YT.PlayerState.PAUSED) {
            videoSkipped(lastPlayerTime, ytplayer.getCurrentTime());
        }
        videoPaused();
        break;
    	case (YT.PlayerState.ENDED):
        videoEnded();
        break;
    	case (YT.PlayerState.UNSTARTED):
        break;
    }
    lastPlayerTime = ytplayer.getCurrentTime();
    lastPlayerState = newState;
}

 
From here, we can call videoStarted, videoPaused, and videoEnded. These send very simple xAPI statements (see http://rusticisoftware.github.io/TinCanJS/). We also keep track of the player’s previous time and state. This lets us send some meaningful statements about what parts of the video the user watched, and which parts were skipped over.

Protip: Call Google’s data api for further information about the video, like the title.

$.getJSON('https://gdata.youtube.com/feeds/api/videos/' + videoID + '?v=2&alt=json',
            function (data) {
                console.log(data);
			videoTitle = data.entry.title.$t;
                //do some stuff here
            }
);

 

Using the TinCanJS library, we can send statements corresponding to the actual part of the video watched. We pass in the start and end times as extensions. Similarly, to send a statement where the user skips a part of the video, we would send the start and end times of the section skipped as extensions.

function videoWatched(start, finish) {//start and finish in seconds
    window.tincan.sendStatement({
        actor: Cards.getActor(),
        verb: {
            id: "http://activitystrea.ms/schema/1.0/watch",
            display: {'en-US': 'watched'}
        },
        target: {
            id: 'http://www.youtube.com/watch?v=' + videoID,
            definition: {
                name: { "en-US": videoTitle + " from " + timeString(start) + " to " + timeString(finish) },
                extensions: {
                    "http://demo.watershedlrs.com/tincan/extensions/start_point": timeString(start),
                    "http://demo.watershedlrs.com/tincan/extensions/end_point": timeString(finish)
                }
            }
        }
    });
}

 
There is a ton of other information that we can get from this api. We can get keywords, playlists, subscriptions, comments, pretty much anything. We can use this information to make the xAPI statements readable and meaningful. “Ervin watched http://www.youtube.com/watch?v=AmC9SmCBUj4” now becomes “Ervin watched Gordon Ramsay: How to Cook the Perfect Steak”

Another hurdle we might encounter is capturing when the user is actually watching the video. If the user clicks play and then leaves the window or minimizes the browser, we want to know that they have taken the main window out of focus.

In traditional content this is pretty straightforward and done with the window’s blur and focus events. These events should send statements indicating that the user suspended and resumed the activity, respectively. But when we embed the YouTube player in an iframe this adds a bit of complexity. This is because the clicking inside the iframe triggers the main window’s blur event. Essentially the iframe does not count as part of the window. To get past this we keep track of when the mouse enters and exits the iframe. For further information on this I suggest this thread.

 
  • Pingback: YouTube + xAPI = Amazing - Experience API()

  • Seth Hoveland

    Cards.getActor()? Maybe I need to dig into the repository a little bit, but this doesn’t look familiar. Is it included in TinCanJS, or is it something that you guys built outside of it?

  • Ervin Puskar

    No, this is not included in TinCanJS. I have a file that happened to be named cards.js, it has a few helper functions, one of which returns the actor information (in json) for the current user that is authenticated.

  • Min Nay Zaw

    how to delete this my photo?

  • Min Nay Zaw

    Dear admin, please delete my photo if possible

  • Ervin Puskar

    Hi Min Nay Zaw,

    What errors are you seeing when you try to run your code?

    Here are a few links to get you started with TinCanJS

    http://rusticisoftware.github.io/TinCanJS/

    http://rusticisoftware.github.io/TinCanJS/doc/api/latest/

    hope this helps!

    Ervin

  • Min Nay Zaw

    Dear Mr Ervin Puskar, I just copy the above of your code into my program. But I cannot see any statement or activities in my LRS (Learning Record Store) from youtube video that I do some activities such as play, pause, stop, etc.

  • Min Nay Zaw

    Dear Mr Ervin Puskar, Please check the following of my code. The statement of youtube is still not displayed in my LRS.

    You need Flash player 8+ and JavaScript enabled to view this video.

    ////////////////////////////////////////////////////////////////////////////////////////////////////////////

    var params = { allowScriptAccess: “always” };

    var atts = { id: “myytplayer” };

    swfobject.embedSWF(“http://www.youtube.com/v/VGxqNbtlNYo?enablejsapi=1&playerapiid=ytplayer&version=3”,

    “ytapiplayer”, “425”, “356”, “8”, null, null, params, atts);

    ///////////////////////////////////////////////////////////////////////////////////////////////////////////

    function onYouTubePlayerReady(playerId) {

    ytplayer = document.getElementById(“myytplayer”);

    ytplayer.addEventListener(“onStateChange”,”onPlayerStateChange”);

    //player loaded

    //do some stuff here if you want!

    }

    ///////////////////////////////////////////////////////////////////////////////////////////////////////////

    function onPlayerStateChange(newState) {

    switch (newState) {

    case (YT.PlayerState.PLAYING):

    videoStarted();

    break;

    case (YT.PlayerState.PAUSED):

    if (lastPlayerState == YT.PlayerState.PLAYING) {

    videoWatched(lastPlayerTime, ytplayer.getCurrentTime())

    } else if (lastPlayerState == YT.PlayerState.PAUSED) {

    videoSkipped(lastPlayerTime, ytplayer.getCurrentTime());

    }

    videoPaused();

    break;

    case (YT.PlayerState.ENDED):

    videoEnded();

    break;

    case (YT.PlayerState.UNSTARTED):

    break;

    }

    lastPlayerTime = ytplayer.getCurrentTime();

    lastPlayerState = newState;

    }

    ///////////////////////////////////////////////////////////////////////////////////////////////////////////

    var myTinCan = new TinCan();

    var myLRS = new TinCan.LRS({

    endpoint:”https://cloud.scorm.com/tc/BMW08XIL47/”,

    version: “1.0.0”,

    auth: ‘Basic ‘ + Base64.encode(“BMW08XIL47” + ‘:’ + “Ze0FxSxLSgaFJpvnjGwAY1Lp5WRSHKItVA4SHstl”)

    });

    myTinCan.recordStores[0] = myLRS;

    //Set the default actor

    var myActor = new TinCan.Agent({

    name : “New User”,

    mbox : “mailto:newuser@gmail.com”

    });

    myTinCan.actor = myActor;

    ///////////////////////////////////////////////////////////////////////////////////////////////////////////

    $.getJSON(‘https://gdata.youtube.com/feeds/api/videos/VGxqNbtlNYo?v=2&alt=json’,

    function (data) {

    console.log(data);

    videoTitle = data.entry.title.$t;

    //do some stuff here

    }

    );

    ///////////////////////////////////////////////////////////////////////////////////////////////////////////

    function videoWatched(start, finish) {//start and finish in seconds

    myTinCan.sendStatement({

    actor : myActor,

    verb: {

    id: “http://activitystrea.ms/schema/1.0/watch”,

    display: {‘en-US’: ‘watched’}

    },

    target: {

    id: ‘http://www.youtube.com/watch?v=VGxqNbtlNYo’,

    definition: {

    name: { “en-US”: videoTitle + ” from ” + timeString(start) + ” to ” + timeString(finish) },

    extensions: {

    “http://demo.watershedlrs.com/tincan/extensions/start_point”: timeString(start),

    “http://demo.watershedlrs.com/tincan/extensions/end_point”: timeString(finish)

    }

    }

    }

    });

    }

  • Ervin,
    Can you upload an example json statement generated from this example? I’m having trouble understanding some specific areas of the xAPI statement like contextareas, etc.

  • Andrew Downes

    Hi Zachary – great question!

    This blog is more around the technical challenges of getting data out of YouTube rather than the specific xAPI data sent. The best place to look at the xAPI data is the Video recipe in the registry: https://registry.tincanapi.com/#profile/19 which lists some verbs, activity types and extensions.

    For help with the structure of statements in general, our Statement Anatomy blog series is a really valuable read (start with the oldest and work through): https://experienceapi.com/category/statement-anatomy/page/3/

    You may also find our prototypes helpful: https://experienceapi.com/prototypes/ I’m actually hoping to get these updated soon so it’s worth checking back in a couple of weeks for more up to date versions.

    I hope that helps? Feel free to reply here or email info@tincanapi.com if you have further questions.

  • Andrew,

    I understand that you don’t want to divert from the original article topic but a more complex example of Experience API statement for ‘Jeffrey Horne played ‘Gordon Ramsay: How to Cook the Perfect Steak’ would help (e.g. https://experienceapi.com/2013/08/27/youtube-tin-can-amazing/). I’m trying to figure out how much detail is needed. I’m trying to use the Video Profile (https://registry.tincanapi.com/#profile/19) to gather as much info as possible. Any help would be appreciated.

  • Andrew Downes

    Hi Zachary,

    Example statements work really badly in blog comments. Can you drop me a mail andrew.downes@tincanapi.com and I can send you a collection of example statements in a text file.

    Thanks!

    Andrew

  • Andrew Downes

    Actually, here’s a json file containing an array of statements you can download: https://drive.google.com/file/d/0BxHeqieRTaoJTW9NTlBKS2R1WXM/view?usp=sharing

  • Andrew Downes

    I think you’ve now got this Zachary, but in case anybody else follows this thread, we’ve also put an example up at https://registry.tincanapi.com/recipes/latest/# so you can see the YouTube tracking in action. This is set to report to the public LRS at https://experienceapi.com/public-lrs/ and if you want to report to a different endpoint you can get the code yourself from https://github.com/RusticiSoftware/TinCanRecipes

    Hope that helps!

  • We have started a Video Community of Practice with the support and supervision of ADL, to decide a common way of tracking all Videos with xAPI. Everyone interested in Video Tracking should be part of the group. http://bit.ly/15sVfXb , And it’s Free 😉

  • blumonkey

    Dear Ervin,
    Great blog on how to get started on Video Tracking. I was wondering whether we could do this on the YouTube domain itself. Like, AFAIK we are embedding a YouTube video on our page and then sending statements regarding the PlayerState etc. Is it possible to track the user even when he/she is on http://www.youtube.com,( automatically, like we did here)?

    Thanks 🙂

  • Ervin Puskar

    Not sure what all the available You Tube api’s are. But I would doubt that You Tube would let you track what each user is doing on your channel. I this at the very most you could get something from google analytics and possibly send statements about that. But you would not be able to identify users uniquely. To get the level of detail you would need to send meaningful statements it is best to embed the video (or videos) in a player you control.