An example of recording iframe-embedded video player states for both YouTube and Vimeo.
A Pen by Jake Albaugh on CodePen.
<header> | |
<div class=container> | |
<div class=col-full> | |
<h1>Tracking Interaction in YouTube and Vimeo iFrame APIs</h1> | |
<p>Interact with the videos below and watch the data update. The behavior for both players is very similar, with a few discrepancies as to when different events fire (see below).</p> | |
</div> | |
</div> | |
</header> | |
<div class=container> | |
<div class=col-full> | |
<p>In the data, <code>sequence</code> is a history of the interaction with completed percentages when the event was triggered. <code>furthest</code> is the highest percentage of completion achieved in the interaction. <code>stops</code> is a count of the amount of times the video complete event fired. These metrics should be enough to derive an accurate picture of how the interaction unfolded; however, they are dependent on the pause, play, and stop events. If you were to navigate from the page while the video is playing, progress would not be updated.</p> | |
<p>This Vimeo approach uses their suggested <a href="https://github.com/vimeo/player-api/tree/master/javascript" target=blank>froogaloop</a> plugin. Vimeo API Documentation can be found on <a href="https://developer.vimeo.com/player/js-api" target=blank>developer.vimeo.com</a>. YouTube's documentation can be found on <a href="https://developers.google.com/youtube/iframe_api_reference" target=blank>developers.google.com</a>. Oh, and <a href="http://www.ratatatmusic.com/" target=blank>Ratatat</a> is amazing.</p> | |
</div> | |
</div> | |
<div class=container> | |
<div class=col> | |
<div id=youtube_player></div> | |
<pre id=yt-output></pre> | |
</div> | |
<div class=col> | |
<iframe id=vimeo_player src="https://player.vimeo.com/video/125104138?api=1&player_id=vimeo_player" width=500 height=281 frameborder=0 webkitallowfullscreen mozallowfullscreen allowfullscreen></iframe> | |
<pre id=vm-output></pre> | |
</div> | |
</div> | |
<footer> | |
<div class=container> | |
<div class=col-full> | |
<h2>Discrepancies</h2> | |
<ul> | |
<li>Vimeo player indicates the <code>stop</code> event and <code>play</code> event after <code>stop</code> are at <code>100.2%</code>.</li> | |
<li>YouTube player indicates <code>stop</code> is at <code>100%</code> and <code>play</code> after <code>stop</code> is at <code>0%</code>. | |
<li>Vimeo player fires two <code>stop</code> events on finish.</li> | |
<li>YouTube player fires an additional <code>play</code> event if you skip ahead past the buffer.</li> | |
<li>YouTube player fires an additional <code>pause</code> event when skipping ahead while paused.</li> | |
</ul> | |
</div> | |
</div> | |
</footer> |
// char entity "icons" for less chars | |
var i_play = "▸", | |
i_pause = "▮▮", | |
i_stop = "▪"; | |
// | |
// YouTube iFrame | |
// API documentation: | |
// https://developers.google.com/youtube/iframe_api_reference | |
// | |
// This code loads the IFrame Player API code asynchronously. | |
var tag = document.createElement('script'); | |
var yt_video_data = { | |
"sequence": [], | |
"furthest": 0, | |
"stops": 0 | |
}; | |
tag.src = "https://www.youtube.com/iframe_api"; | |
var firstScriptTag = document.getElementsByTagName('script')[0]; | |
firstScriptTag.parentNode.insertBefore(tag, firstScriptTag); | |
// This function creates an <iframe> (and YouTube player) | |
// after the API code downloads. | |
var yt_player; | |
function onYouTubeIframeAPIReady() { | |
yt_player = new YT.Player('youtube_player', { | |
height: '281', | |
width: '500', | |
videoId: 'f7wkRET0hbo', | |
// we only need the state change event | |
events: { | |
'onStateChange': onPlayerStateChange | |
} | |
}); | |
} | |
// The API calls this function when the player's state changes. | |
// The function indicates that when playing a video (state=1), | |
function onPlayerStateChange(event) { | |
if (event.data == YT.PlayerState.PLAYING) { | |
updateYtVideoData(i_play); | |
} else if (event.data == YT.PlayerState.PAUSED) { | |
updateYtVideoData(i_pause); | |
} else if (event.data == YT.PlayerState.ENDED) { | |
updateYtVideoData(i_stop); | |
} | |
} | |
// tracking interaction data | |
function updateYtVideoData(which) { | |
// getting video progress | |
var progress = ytVideoProgress(); | |
// set furthest if progress is the furthest | |
yt_video_data.furthest = Math.max(yt_video_data.furthest, progress); | |
// add current video progress to sequence | |
yt_video_data.sequence.push([which, progress]) | |
// if video is complete | |
if (which == i_stop) { yt_video_data.stops++; yt_video_data.furthest = 100; } | |
// put output in dom | |
ytPrintData(); | |
} | |
// printing the video data | |
function ytPrintData() { | |
var output = ""; | |
for(var key in yt_video_data) { | |
output += key + ": " + JSON.stringify(yt_video_data[key]) + "\n"; | |
} | |
document.getElementById("yt-output").innerHTML = output; | |
} | |
// getting video progress in 0-100 percentage value | |
function ytVideoProgress() { | |
var ratio = yt_player.getCurrentTime() / yt_player.getDuration(), | |
percent = ratio * 100, | |
round_percent = Math.round(percent * 10) / 10; | |
return round_percent; | |
} | |
// initial call | |
ytPrintData(); | |
// | |
// Vimeo player | |
// requires Vimeo's froogaloop | |
// API documentation: | |
// https://developer.vimeo.com/player/js-api | |
// | |
var vm_video_data = { | |
"sequence": [], | |
"furthest": 0, | |
"stops": 0 | |
}; | |
var player = document.getElementById('vimeo_player'); | |
$f(player).addEvent('ready', ready); | |
// crossbrowser event listener, thanks vimeo | |
function addEvent(element, eventName, callback) { | |
if (element.addEventListener) { | |
element.addEventListener(eventName, callback, false); | |
} | |
else { | |
element.attachEvent(eventName, callback, false); | |
} | |
} | |
// when player is ready | |
function ready(player_id) { | |
var player = $f(player_id), | |
duration = 0; | |
player.addEvent('pause', onPause); | |
player.addEvent('finish', onStop); | |
player.addEvent('play', onPlay); | |
player.api('getDuration', function (value, id) { | |
duration = value; | |
}); | |
function onPlay(id) { updateVmVideoData(i_play); } | |
function onPause(id) { updateVmVideoData(i_pause); } | |
function onStop(id) { updateVmVideoData(i_stop); } | |
// called on each event | |
function updateVmVideoData(which) { | |
player.api('getCurrentTime', function (time, id) { | |
// video progress | |
var progress = vmVideoProgress(time); | |
// set furthest value if progress is greater | |
vm_video_data.furthest = Math.max(vm_video_data.furthest, progress); | |
// add current data to sequence | |
vm_video_data.sequence.push([which, progress]); | |
// if video has ended | |
if(which == i_stop) { vm_video_data.stops++; vm_video_data.furthest = 100.2;}; | |
// print vimeo data in dom | |
vmPrintData(); | |
}); | |
} | |
// yielding a progress in 0-100 percentage format | |
function vmVideoProgress(time) { | |
var ratio = time / duration, | |
percent = ratio * 100, | |
round_percent = Math.round(percent * 10) / 10; | |
return round_percent; | |
} | |
// printing the video data | |
function vmPrintData() { | |
var output = ""; | |
for(var key in vm_video_data) { | |
output += key + ": " + JSON.stringify(vm_video_data[key]) + "\n"; | |
} | |
document.getElementById("vm-output").innerHTML = output; | |
} | |
// initial print | |
vmPrintData(); | |
} | |
jakealbaughSignature(); |
<script src="https://f.vimeocdn.com/js/froogaloop2.min.js"></script> | |
// all of this is for the demo. you can ignore. | |
@import url(http://fonts.googleapis.com/css?family=Open+Sans:300); | |
body { | |
background: white; | |
color: #111; | |
line-height:1.6; | |
font-family: "Open Sans", sans-serif; | |
font-weight: 300; | |
text-shadow: 1px 1px 0px rgba(255,255,255,0.2); | |
} | |
header { | |
background: #FFBB00; | |
margin-bottom: 3em; | |
padding: 4em 0 2em; | |
border-bottom: 1px solid rgba(255,255,255,0.2); | |
box-shadow: 0px 0px 12px 4px rgba(0,0,0,0.1); | |
h1 { | |
margin-bottom: 1em; | |
} | |
p { margin: 0; } | |
} | |
footer { | |
padding: 2em 0; | |
margin-top: 2em; | |
background: #111; | |
color: white; | |
text-shadow: none; | |
ul { margin: 0; padding-left: 1.5em; } | |
h2 { margin-bottom: 1em; margin-top: 0; } | |
} | |
h1 { | |
font-weight: 300; | |
line-height: 1.2; | |
margin: 0 0 1.5em; | |
} | |
a { color: #ffbb00; text-decoration: none; } | |
iframe { | |
margin: 2em auto; | |
width: 100%; | |
display: block; | |
} | |
iframe, pre { | |
box-shadow: 0px 4px 12px 0px rgba(0,0,0,0.1); | |
} | |
code { | |
padding: 0.15em 0.25em 0.25em; | |
margin-top: -0.3em; | |
display: inline-block; | |
font-size: 0.8em; | |
border-radius: 2px; | |
line-height: 1; | |
background: #222; | |
} | |
pre, code { | |
color: #FFBB00; | |
text-shadow: none; | |
} | |
pre { | |
font-size: 12px; | |
width: 100%; | |
margin: 0 auto; | |
padding: 1em; | |
box-sizing: border-box; | |
background: #111; | |
} | |
.container { | |
margin: 0 auto; | |
width: 90%; | |
&::after { content: ''; display: table; clear: both; } | |
} | |
.col, .col-full { | |
width: 100%; | |
margin: 0 auto; | |
box-sizing: border-box; | |
padding: 0px 10px; | |
} | |
@media (min-width: 1040px) { | |
.col { float: left; width: 50%; padding: 0 10px 2em; } | |
.container { width: 1040px; } | |
} | |
An example of recording iframe-embedded video player states for both YouTube and Vimeo.
A Pen by Jake Albaugh on CodePen.