Urban Insight operates a highly successful on-demand video service, Planetizen Courses. The service keeps improving rapidly; it’s not uncommon for new features to appear on the site. One recent enhancement allows the system to keep track of progression in videos as they are being watched.
This enhancement allows for two new features:
- Users are able to keep track of their progression during a course, which may consist of many videos.
- Courses can now offer completion certificates to users who have completed certain courses.
How to Implement Vimeo Progression Tracking
Planetizen Courses relies on Vimeo to deliver its video content. Vimeo offers a very rich JavaScript API for developers, which provided everything we needed to implement progression tracking.
Vimeo’s JavaScript library is called Froogaloop, and it’s available on GitHub. The way you use the library is similar to that of jQuery in the sense that it allows you to wrap a DOM object in a Froogaloop object, which will add the API functions to that object. Assuming the embedded video is in an iframe with the id #myvideo, initializing the Froogaloop library might look as follows:
var videoIframe = $('#myvideo'); var video = $f(videoIframe[0]);
Having done this, you can now call the Froogaloop functions on the video object, such as video.api(‘play’). The api() function on the object calls a certain Vimeo API method, so methods you look up in the API documentation will need to be passed to api() instead of being called directly on the object. For instance, the method to start the video is play(), so in order to call that, you will need to call video.api(‘play’) instead of video.play(). Similarly, event listeners can be registered using the addEvent() method of the Froogaloop library.
Before we do anything to the video, we need to wait for the ready event to fire, which will assure that the video is fully loaded. Adding an event listener for the ready event will look as follows.
video.addEvent('ready', vimeoReadyEvent);
vimeoReadyEvent is the function that will be called when the event triggers. Every other video related call needs to happen after that.
In order for us to be able to track progression in the video, we will need to be notified when the video is being played. There are three related events that are fired by the API.
- play: Fired when the video starts playing.
- pause: Fired when the video stops playing.
- playProgress: Fired periodically during playback, providing information on the progress.
Let’s register a listener function for the playProgress event.
video.addEvent('playProgress', vimeoPlayProgressEvent);
The definition of the playProgress function should look like vimeoPlayProgressEvent(data), and the following object can be expected as data:
{ seconds: 137.925, percent: 0.2, duration: 688.32 }
where seconds is the time location currently being played; percent is the percentage location currently being played (normalized to 1), and duration is the total video duration.
On Courses, we wish to keep track of each section of the video that the user has watched so that watching the first half of the video twice will not qualify as completing the video. Therefore, each time the playProgress event fires, we store the percentage location it reports in an array. That way we end up with an array that contains the locations as percentage values of all the video sections that have been watched. For example, if the user watches the first three percent of the video, the array will contain [0, 1, 2]. If they then jump to the middle, and watch another 3 percent, the array will contain [0, 1, 2, 50, 51, 52]. Given this array, we can easily calculate what overall percentage of the video has been watched by counting the items in the array. For this, we need the following listener function:
plnzVideoTracker.progress = []; vimeoPlayProgressEvent: function (data) { var currentPercent = Math.ceil(data.percent * 100); if (plnzVideoTracker.progress.indexOf(currentPercent) != -1) { return; } plnzVideoTracker.progress.push(currentPercent); },
So far, the data is only available on the client side; we will want to send it back to the server. The playProgress event fires very often, so it would not be efficient to send the data each time. Instead, we set up a timer that triggers a server update every 30 seconds. The data is sent back to the server using a post request.
setInterval(updateProgress, 30000); updateProgress: function () { $.post('/callback', { progress: plnzVideoTracker.progress }); }
With these set up, we have a working progression tracking feature on the frontend, but there are two potential issues:
- If the user quickly stops the video and navigates away, we may lose 30 seconds of data because the script does not have a chance to post it back to the server.
- If the user keeps the video page open, but does not watch the video, the same set of data will be posted back to the server repeatedly.
To address the first issue, we set up a listener function for the pause event, which posts the data as soon as the video pauses.
video.addEvent('pause', vimeoPauseEvent); vimeoPauseEvent: function (data) { updateProgress(); },
To eliminate the second issue, we note the timestamp of the last time that the progress was updated, and only send data to the server if there has been any progress since the last time. This necessitates changes to the vimeoPlayProgressEvent and the updateProgress function.
vimeoPlayProgressEvent: function (data) { var currentPercent = Math.ceil(data.percent * 100); if (plnzVideoTracker.progress.indexOf(currentPercent) != -1) { return; } var timestamp = (new Date()).getTime(); timestamp = Math.floor(timestamp / 1000); plnzVideoTracker.progress.push(currentPercent); plnzVideoTracker.lastUpdate = timestamp; } updateProgress: function () { if ( plnzVideoTracker.lastSent == plnzVideoTracker.lastUpdate) { return; } plnzVideoTracker.lastSent = plnzVideoTracker.lastUpdate; $.post('/callback', { progress: plnzVideoTracker.progress }); }
As far as the frontend is concerned, the code snippets discussed here complete the progression tracking functionality.