YouTube Event Logging

April 25th, 2010 / Utilities

A few months ago, a project bid had me looking at YouTube’s JavaScript API for ways to manage a video library through a custom interface. Most of the videos were over 30 minutes, and I thought it would be great if I could provide some analytics on how users were interacting with the videos. Were people getting bored and leaving at a certain spot, rewinding interesting parts, or maybe randomly scrubbing through it?

By monitoring the player’s state changes, I made some headway with logging user actions. There were still some major problems, which I’ll go over in a bit, and after the bid fell through I stopped working on it. After revisiting it this week, I can now reliably log the following:

  • Where a user starts watching
  • Pause and resume times
  • Start and end times when scrubbing
  • When a user changes the video quality
  • When it needs to pause for buffering
  • If it plays through to the end
  • Where in the video does someone leave the page

Demo

Interact with the video and watch the log on the right.

Applications

I had Clicky in mind when I started working on this. It already provided detailed information on each visitor action and manual logging for in-page events, so being able to see what people were doing within the video would be voyeuristic delight.

YouTube Event Logging

Event logging in Clicky

Method

The key to everything is the onStateChange event, which alerts you to every state change in the player. Possible values are unstarted (-1), ended (0), playing (1), paused (2), buffering (3), and video cued (5). Combined with player.getCurrentTime(), you can figure out what a user is doing, and when they are doing it. Keeping track of what the previous event was lets you determine whether a state of 1 is the player starting, resuming, or scrubbing.

Problem

This was all well and good in theory, but getting the exact time of the actions proved to be very difficult at first. For example, when I scrub a video to some future point in the timeline, the state changes from 1-2-3-1: playing, pause, buffer, play. Capturing the start time of the scrub is simple, but even though the last state will be 'playing’, the video will not actually play until the browser has downloaded enough to resume the stream. So a state of 1 ends up meaning 'trying to play’ and not necessarily 'playing’. Depending on network speeds and video quality, the player might sit on a state of 1 anywhere from 1 – 20 seconds before the video really is playing, and the video has to start playing again in order to capture the end scrub time.

I first tried to fix this problem with timers… Wait 5 seconds after last 'play’ state to grab the time, subtract 2.5, and IF the video was in fact playing by then, I’d have a time to use that was accurate +/- 2.5 seconds. Yeah, that didn’t work very well. Higher quality streams and unpredictable network conditions made a timer-based solution terribly undependable.

Solution

The only way to reliably capture action timing was to monitor the video quality of the stream, and how much was loaded, via player.getPlaybackQuality() and player.getVideoBytesLoaded(). Playback quality will return either Small, Medium, Large, HD720, or HD1080. Based on quality, I determine when a video is playing based on rules of how much should be loaded. For example: If the stream is 'Large’, video will play after 300KB is loaded, if 'HD720′, 800KB. If you seek to a position in the timeline that has already loaded, there is no problem – video plays immediately. If seeking to a position that had not loaded yet, then 'bytes loaded’ resets and starts loading video at the new position, and once it crosses the threshold, it SHOULD be playing and I capture the time.

Caveats

I said SHOULD because it isn’t always perfect. For one thing, all streams are not created equal. One 'Medium’ video might start playing after 100KB loaded, another might be 300KB. I’ve done some testing on each quality level, and the limits I’ve set are on the conservative side. It might start playing at 100KB, but the script will be waiting for 300KB, so the time will be off by a few seconds.

The opposite holds true as well. The video might not start playing at 300KB, but rather 400KB, and misfire the player.getCurrentTime() call. In that instance the start and end times of the scrub will be the same. The conservative limits I’ve set should prevent this from happening, but you never know. The function that controls this is setByteMeter(), so tweak it if you wish.

This would have been a lot easier if returning a state of 'playing’ really meant that, and not that it was thinking about it.

Introspection

Once again I’ve solved a problem that no longer exists, as I have no practical use for this right now.

Update

Clicky ended up using this code as a base for the YouTube portion of their video analytics service. I’m happy it was put to real use, not just sitting here as another time-waster idea.