Last active
August 13, 2017 23:19
-
-
Save nathanwoulfe/6d57b35c8e66beb237b649b99284c25f to your computer and use it in GitHub Desktop.
An example AngularJs directive using the YouTube iFrame API, pushing events into the GTM datalayer
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// functional demo at http://www.usc.edu.au/open-day#sessions | |
(function() { | |
'use strict' | |
function hubVideo() { | |
var video = { | |
restrict: 'E', | |
replace: true, | |
scope: { | |
youtube: '=', | |
summary: '=', | |
timeline: '=', | |
title: '=' | |
}, | |
template: [ | |
'<div class="video-with-timeline">', | |
' <div class="embed-responsive embed-responsive-16by9">', | |
' <div id="player"></div>', | |
' </div>', | |
' <h4 ng-if="title && !timeline" ng-bind="title"></h4>', | |
' <p ng-if="summary && !timeline" ng-bind="summary"></p>', | |
' <ul class="video-timeline" ng-if="timeline">', | |
' <li ng-repeat="t in timeline track by $index" ng-class="{\'active\' : $index === activeSlide - 1}" ng-click="scrub(t.time)" title="{{ t.label }}"><span>{{ $index + 1 }}</span></li>', | |
' </ul> {{ elapsed }}', | |
'</div>' | |
].join(''), | |
link: link | |
}; | |
function link(scope) { | |
// ensure dataLayer exists - youtube API is loaded elsewhere and will be available when this code executes | |
if (!dataLayer) { | |
dataLayer = []; | |
} | |
// var | |
var time, player, elapsed = 0; | |
scope.activeSlide = 0; | |
// track the elapsed time and update the active chapter li element | |
// the timeline data is provided to the directive in scope.timeline | |
function startWatch() { | |
time = setInterval(function() { | |
elapsed = player.getCurrentTime(); | |
scope.activeSlide = scope.timeline.map(function(v, i) { | |
var min = i === 0 ? scope.timeline[0].time : v.time; | |
return elapsed >= min; | |
}).lastIndexOf(true) + 1; | |
scope.$apply(); | |
}, 100); | |
} | |
// GTM struggles to track dynamic embeds, so let's fire off our own datalayer events | |
function doGtmStuff(e) { | |
e['data'] === YT.PlayerState.PLAYING && setTimeout(onPlayerPercent, 1000, e['target']); | |
var videoData = e.target['getVideoData'](), | |
label = videoData.video_id + ':' + videoData.title; | |
// report play button clicks | |
if (e['data'] === YT.PlayerState.PLAYING && YT.gtmLastAction === 'p') { | |
dataLayer.push({ | |
event: 'youtube', | |
action: 'play', | |
label: label | |
}); | |
} | |
YT.gtmLastAction = ''; | |
// report pause button clicks | |
if (e['data'] === YT.PlayerState.PAUSED) { | |
dataLayer.push({ | |
event: 'youtube', | |
action: 'pause', | |
label: label | |
}); | |
YT.gtmLastAction = 'p'; | |
} | |
} | |
function stopWatch() { | |
clearTimeout(time); | |
} | |
// when something goes wrong, let GTM know about it | |
function onError(e) { | |
dataLayer.push({ | |
event: 'error', | |
action: 'GTM', | |
label: 'youtube:' + e['target']['src'] + '-' + e['data'] | |
}); | |
} | |
// report the % played if it matches 0%, 25%, 50%, 75% or completed | |
// Change the % to more increments 5% (0%, 5%, 10% etc) as the video auto video is long | |
function onPlayerPercent(e) { | |
if (e['getPlayerState']() === YT.PlayerState.PLAYING) { | |
var t = e['getDuration']() - e['getCurrentTime']() <= 1.5 ? 1 : (Math.floor(e['getCurrentTime']() / e['getDuration']() * 20) / 20).toFixed(2); | |
if (!e['lastP'] || t > e['lastP']) { | |
var videoData = e['getVideoData'](), | |
label = videoData.video_id + ':' + videoData.title; | |
e['lastP'] = t; | |
dataLayer.push({ | |
event: 'youtube', | |
action: Math.round(t * 100) + '%', | |
label: label | |
}); | |
} | |
e['lastP'] !== 1 && setTimeout(onPlayerPercent, 1000, e); | |
} | |
} | |
// when everything is loaded, start playing the video | |
function onPlayerReady(event) { | |
event.target.playVideo(); | |
YT.gtmLastAction = 'p'; | |
} | |
// if the video has a timeline, we need to observe the elapsed time to set the correct chapter as active - this happens in startWatch | |
function onStateChange(e) { | |
if (scope.timeline) { | |
if (e.data == YT.PlayerState.PLAYING) { | |
startWatch(); | |
} else { | |
stopWatch(); | |
} | |
} | |
doGtmStuff(e); | |
} | |
// clicking a timeline chapter sets the current play time | |
scope.scrub = function(time) { | |
player.seekTo(time); | |
} | |
// create the player object with sane default values | |
// also binds events - we track inside the callback functions | |
player = new YT.Player('player', { | |
height: '390', | |
width: '640', | |
videoId: scope.youtube, | |
playerVars: { | |
'autoplay': 1, | |
'rel': 0, | |
'modestbranding': 1, | |
'showinfo': 0 | |
}, | |
events: { | |
'onReady': onPlayerReady, | |
'onStateChange': onStateChange, | |
'onError': onError | |
} | |
}); | |
} | |
return video; | |
} | |
angular.module('app').directive('hubVideo', [hubVideo]); | |
}()); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment