Skip to content

Instantly share code, notes, and snippets.

@adrianotadao
Created November 22, 2015 11:42
Show Gist options
  • Save adrianotadao/34d64ec633f35141b29f to your computer and use it in GitHub Desktop.
Save adrianotadao/34d64ec633f35141b29f to your computer and use it in GitHub Desktop.
/**
* Copyright 2014 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* IMA SDK integration plugin for Video.js. For more information see
* https://www.github.com/googleads/videojs-ima
*/
(function(vjs) {
'use strict';
var extend = function(obj) {
var arg;
var index;
var key;
for (index = 1; index < arguments.length; index++) {
arg = arguments[index];
for (key in arg) {
if (arg.hasOwnProperty(key)) {
obj[key] = arg[key];
}
}
}
return obj;
},
defaults = {
},
imaPlugin = function(options, readyCallback) {
var player = this;
/**
* Creates the ad container passed to the IMA SDK.
* @ignore
*/
player.ima.createAdContainer_ = function() {
// The adContainerDiv is the DOM of the element that will house
// the ads and ad controls.
vjsControls = player.getChild('controlBar');
adContainerDiv =
vjsControls.el().parentNode.insertBefore(
document.createElement('div'),
vjsControls.el());
adContainerDiv.id = 'ima-ad-container';
adContainerDiv.style.display = 'none';
adContainerDiv.style.width = player.width() + 'px';
adContainerDiv.style.height = player.height() + 'px';
adContainerDiv.addEventListener(
'mouseover',
player.ima.showAdControls_,
false);
adContainerDiv.addEventListener(
'mouseout',
player.ima.hideAdControls_,
false);
player.ima.createControls_();
adDisplayContainer =
new google.ima.AdDisplayContainer(adContainerDiv, contentPlayer);
};
/**
* Creates the controls for the ad.
* @ignore
*/
player.ima.createControls_ = function() {
controlsDiv = document.createElement('div');
controlsDiv.id = 'ima-controls-div';
controlsDiv.style.width = '100%';
countdownDiv = document.createElement('div');
countdownDiv.id = 'ima-countdown-div';
countdownDiv.innerHTML = 'Advertisement';
seekBarDiv = document.createElement('div');
seekBarDiv.id = 'ima-seek-bar-div';
seekBarDiv.style.width = player.width() + 'px';
progressDiv = document.createElement('div');
progressDiv.id = 'ima-progress-div';
playPauseDiv = document.createElement('div');
playPauseDiv.id = 'ima-play-pause-div';
playPauseDiv.className = 'ima-playing';
playPauseDiv.addEventListener(
'click',
player.ima.onAdPlayPauseClick_,
false);
muteDiv = document.createElement('div');
muteDiv.id = 'ima-mute-div';
muteDiv.className = 'ima-non-muted';
muteDiv.addEventListener(
'click',
player.ima.onAdMuteClick_,
false);
fullscreenDiv = document.createElement('div');
fullscreenDiv.id = 'ima-fullscreen-div';
fullscreenDiv.className = 'ima-non-fullscreen';
fullscreenDiv.addEventListener(
'click',
player.ima.onAdFullscreenClick_,
false);
adContainerDiv.insertBefore(
controlsDiv,
adContainerDiv.childNodes[adContainerDiv.childNodes.length] || null);
controlsDiv.insertBefore(
countdownDiv, controlsDiv.childNodes[controlsDiv.childNodes.length] || null);
controlsDiv.insertBefore(
seekBarDiv, controlsDiv.childNodes[controlsDiv.childNodes.length] || null);
controlsDiv.insertBefore(
playPauseDiv, controlsDiv.childNodes[controlsDiv.childNodes.length] || null);
controlsDiv.insertBefore(
muteDiv, controlsDiv.childNodes[controlsDiv.childNodes.length] || null);
controlsDiv.insertBefore(
fullscreenDiv, controlsDiv.childNodes[controlsDiv.childNodes.length] || null);
seekBarDiv.insertBefore(
progressDiv, seekBarDiv.childNodes[controlsDiv.childNodes.length] || null);
};
/**
* Initializes the AdDisplayContainer. On mobile, this must be done as a
* result of user action.
*/
player.ima.initializeAdDisplayContainer = function() {
adDisplayContainerInitialized = true;
adDisplayContainer.initialize();
}
/**
* Creates the AdsRequest and request ads through the AdsLoader.
*/
player.ima.requestAds = function(adTagUrl) {
if (!adDisplayContainerInitialized) {
adDisplayContainer.initialize();
}
var adsRequest = new google.ima.AdsRequest();
adsRequest.adTagUrl = adTagUrl ? adTagUrl : settings.adTagUrl;
adsRequest.linearAdSlotWidth = player.width();
adsRequest.linearAdSlotHeight = player.height();
adsRequest.nonLinearAdSlotWidth =
settings.nonLinearWidth || player.width();
adsRequest.nonLinearAdSlotHeight =
settings.nonLinearHeight || (player.height() / 3);
adsLoader.requestAds(adsRequest);
};
/**
* Listener for the ADS_MANAGER_LOADED event. Creates the AdsManager,
* sets up event listeners, and triggers the 'adsready' event for
* videojs-ads-contrib.
* @ignore
*/
player.ima.onAdsManagerLoaded_ = function(adsManagerLoadedEvent) {
adsManager = adsManagerLoadedEvent.getAdsManager(
contentPlayheadTracker, adsRenderingSettings);
adsManager.addEventListener(
google.ima.AdErrorEvent.Type.AD_ERROR,
player.ima.onAdError_);
adsManager.addEventListener(
google.ima.AdEvent.Type.CONTENT_PAUSE_REQUESTED,
player.ima.onContentPauseRequested_);
adsManager.addEventListener(
google.ima.AdEvent.Type.CONTENT_RESUME_REQUESTED,
player.ima.onContentResumeRequested_);
adsManager.addEventListener(
google.ima.AdEvent.Type.LOADED,
player.ima.onAdLoaded_);
adsManager.addEventListener(
google.ima.AdEvent.Type.STARTED,
player.ima.onAdStarted_);
adsManager.addEventListener(
google.ima.AdEvent.Type.CLICK,
player.ima.onAdPlayPauseClick_);
adsManager.addEventListener(
google.ima.AdEvent.Type.COMPLETE,
player.ima.onAdComplete_);
player.trigger('adsready');
};
/**
* Start ad playback, or content video playback in the absence of a
* pre-roll.
*/
player.ima.start = function() {
try {
adsManager.init(
player.width(),
player.height(),
google.ima.ViewMode.NORMAL);
adsManager.setVolume(player.muted() ? 0 : player.volume());
adsManager.start();
} catch (adError) {
player.ima.onAdError_(adError);
}
};
/**
* Listener for errors fired by the AdsLoader.
* @param {google.ima.AdErrorEvent} event The error event thrown by the
* AdsLoader. See
* https://developers.google.com/interactive-media-ads/docs/sdks/html5/v3/apis#ima.AdError.Type
* @ignore
*/
player.ima.onAdsLoaderError_ = function(event) {
window.console.log('AdsLoader error: ' + event.getError());
if (adsManager) {
adsManager.destroy();
}
player.play();
};
/**
* Listener for errors thrown by the AdsManager.
* @param {google.ima.AdErrorEvent} adErrorEvent The error event thrown by
* the AdsManager.
* @ignore
*/
player.ima.onAdError_ = function(adErrorEvent) {
window.console.log('Ad error: ' + adErrorEvent.getError());
adsManager.destroy();
adContainerDiv.style.display = 'none';
player.play();
};
/**
* Pauses the content video and displays the ad container so ads can play.
* @param {google.ima.AdEvent} adEvent The AdEvent thrown by the AdsManager.
* @ignore
*/
player.ima.onContentPauseRequested_ = function(adEvent) {
adsActive = true;
adPlaying = true;
player.off('ended', localContentEndedListener);
if (adEvent.getAd().getAdPodInfo().getPodIndex() != -1) {
// Skip this call for post-roll ads
player.ads.startLinearAdMode();
}
adContainerDiv.style.display = 'block';
vjsControls.hide();
player.pause();
};
/**
* Resumes content video and hides the ad container.
* @param {google.ima.AdEvent} adEvent The AdEvent thrown by the AdsManager.
* @ignore
*/
player.ima.onContentResumeRequested_ = function(adEvent) {
adsActive = false;
adPlaying = false;
player.on('ended', localContentEndedListener);
adContainerDiv.style.display = 'none';
vjsControls.show();
if (!currentAd) {
// Something went wrong playing the ad
player.ads.endLinearAdMode();
} else if (!contentComplete &&
// Don't exit linear mode after post-roll or content will auto-replay
currentAd.getAdPodInfo().getPodIndex() != -1 ) {
player.ads.endLinearAdMode();
}
countdownDiv.innerHTML = '';
};
/**
* Starts the content video when a non-linear ad is loaded.
* @param {google.ima.AdEvent} adEvent The AdEvent thrown by the AdsManager.
* @ignore
*/
player.ima.onAdLoaded_ = function(adEvent) {
if (!adEvent.getAd().isLinear()) {
player.play();
}
};
/**
* Starts the interval timer to check the current ad time when an ad starts
* playing.
* @param {google.ima.AdEvent} adEvent The AdEvent thrown by the AdsManager.
* @ignore
*/
player.ima.onAdStarted_ = function(adEvent) {
currentAd = adEvent.getAd();
if (currentAd.isLinear()) {
adTrackingTimer = setInterval(
player.ima.onAdPlayheadTrackerInterval_, 250);
}
};
/**
* Clears the interval timer for current ad time when an ad completes.
* @param {google.ima.AdEvent} adEvent The AdEvent thrown by the AdsManager.
* @ignore
*/
player.ima.onAdComplete_ = function(adEvent) {
if (currentAd.isLinear()) {
clearInterval(adTrackingTimer);
}
};
/**
* Gets the current time and duration of the ad and calls the method to
* update the ad UI.
* @ignore
*/
player.ima.onAdPlayheadTrackerInterval_ = function() {
var remainingTime = adsManager.getRemainingTime();
var duration = currentAd.getDuration();
var currentTime = duration - remainingTime;
currentTime = currentTime > 0 ? currentTime : 0;
var isPod = false;
var adPosition, totalAds;
if (currentAd.getAdPodInfo()) {
isPod = true;
adPosition = currentAd.getAdPodInfo().getAdPosition();
totalAds = currentAd.getAdPodInfo().getTotalAds();
}
// Update countdown timer data
var remainingMinutes = Math.floor(remainingTime / 60);
var remainingSeconds = Math.floor(remainingTime % 60);
if (remainingSeconds.toString().length < 2) {
remainingSeconds = '0' + remainingSeconds;
}
var podCount = ': ';
if (isPod) {
podCount = ' (' + adPosition + ' of ' + totalAds + '): ';
}
countdownDiv.innerHTML =
'Advertisement' + podCount +
remainingMinutes + ':' + remainingSeconds;
// Update UI
var playProgressRatio = currentTime / duration;
var playProgressPercent = playProgressRatio * 100;
progressDiv.style.width = playProgressPercent + '%';
};
/**
* Hides the ad controls on mouseout.
* @ignore
*/
player.ima.hideAdControls_ = function() {
playPauseDiv.style.display = 'none';
muteDiv.style.display = 'none';
fullscreenDiv.style.display = 'none';
controlsDiv.style.height = '14px';
};
/**
* Shows ad controls on mouseover.
* @ignore
*/
player.ima.showAdControls_ = function() {
controlsDiv.style.height = '37px';
playPauseDiv.style.display = 'block';
muteDiv.style.display = 'block';
fullscreenDiv.style.display = 'block';
};
/**
* Listener for clicks on the play/pause button during ad playback.
* @ignore
*/
player.ima.onAdPlayPauseClick_ = function() {
if (adPlaying) {
playPauseDiv.className = 'ima-paused';
adsManager.pause();
adPlaying = false;
} else {
playPauseDiv.className = 'ima-playing';
adsManager.resume();
adPlaying = true;
}
};
/**
* Listener for clicks on the mute button during ad playback.
* @ignore
*/
player.ima.onAdMuteClick_ = function() {
if (adMuted) {
muteDiv.className = 'ima-non-muted';
adsManager.setVolume(1);
// Bubble down to content player
player.muted(false);
adMuted = false;
} else {
muteDiv.className = 'ima-muted';
adsManager.setVolume(0);
// Bubble down to content player
player.muted(true);
adMuted = true;
}
};
/**
* Listener for clicks on the fullscreen button durin ad playback.
* @ignore
*/
player.ima.onAdFullscreenClick_ = function() {
if (player.isFullscreen()) {
player.cancelFullscreen();
} else {
player.requestFullscreen();
}
};
/**
* Listens for the video.js player to change its fullscreen status. This
* keeps the fullscreen-ness of the AdContainer in sync with the player.
* @ignore
*/
player.ima.onFullscreenChange_ = function() {
if (player.isFullscreen()) {
fullscreenDiv.className = 'ima-fullscreen';
adContainerDiv.style.width = window.screen.width + 'px';
adContainerDiv.style.height = window.screen.height + 'px';
adsManager.resize(
window.screen.width,
window.screen.height,
google.ima.ViewMode.FULLSCREEN);
} else {
fullscreenDiv.className = 'ima-non-fullscreen';
adContainerDiv.style.width = player.width() + 'px';
adContainerDiv.style.height = player.height() + 'px';
adsManager.resize(
player.width(),
player.height(),
google.ima.ViewMode.NORMAL);
}
};
/**
* Listens for the video.js player to change its volume. This keeps the ad
* volume in sync with the content volume if the volume of the player is
* changed while content is playing
* @ignore
*/
player.ima.onVolumeChange_ = function() {
var newVolume = player.muted() ? 0 : player.volume();
if (adsManager) {
adsManager.setVolume(newVolume);
}
};
/**
* Seeks content to 00:00:00. This is used as an event handler for the
* loadedmetadata event, since seeking is not possible until that event has
* fired.
* @ignore
*/
player.ima.seekContentToZero_ = function() {
player.off('loadedmetadata', player.ima.seekContentToZero_);
player.currentTime(0);
};
/**
* Seeks content to 00:00:00 and starts playback. This is used as an event
* handler for the loadedmetadata event, since seeking is not possible until
* that event has fired.
* @ignore
*/
player.ima.playContentFromZero_ = function() {
player.off('loadedmetadata', player.ima.playContentFromZero_);
player.currentTime(0);
player.play();
};
/**
* Destroys the AdsManager, sets it to null, and calls contentComplete to
* reset correlators. Once this is done it requests ads again to keep the
* inventory available.
* @ignore
*/
player.ima.resetIMA_ = function() {
if (adsManager) {
adsManager.destroy();
adsManager = null;
}
if (adsLoader && !contentComplete) {
adsLoader.contentComplete();
}
contentComplete = false;
};
/**
* Ads an EventListener to the AdsManager. For a list of available events,
* see
* https://developers.google.com/interactive-media-ads/docs/sdks/html5/v3/apis#ima.AdEvent.Type
* @param {google.ima.AdEvent.Type} event The AdEvent.Type for which to
* listen.
* @param {function} callback The method to call when the event is fired.
*/
player.ima.addEventListener = function(event, callback) {
if (adsManager) {
adsManager.addEventListener(event, callback);
}
};
/**
* Returns the instance of the AdsManager.
* @return {google.ima.AdsManager} The AdsManager being used by the plugin.
*/
player.ima.getAdsManager = function() {
return adsManager;
};
player.ima.getAdsLoader = function() {
return adsLoader;
};
/**
* Sets the content of the video player. You should use this method instead
* of setting the content src directly to ensure the proper ad tag is
* requested when the video content is loaded.
* @param {string} contentSrc The URI for the content to be played.
* @param {?string} adTag The ad tag to be requested when the content loads.
* Leave blank to use the existing ad tag.
* @param {?boolean} playOnLoad True to play the content once it has loaded,
* false to only load the content but not start playback.
*/
player.ima.setContent =
function( contentSrc, adTag, playOnLoad) {
player.ima.resetIMA_();
settings.adTagUrl = adTag ? adTag : settings.adTagUrl;
player.pause();
player.src(contentSrc);
if (playOnLoad) {
player.on('loadedmetadata', function() {
player.ima.playContentFromZero_();
});
} else {
player.on('loadedmetadata', function() {
player.ima.seekContentToZero_();
});
}
};
/**
* Adds a listener for the 'ended' event of the video player. This should be
* used instead of setting an 'ended' listener directly to ensure that the
* ima can do proper cleanup of the SDK before other event listeners
* are called.
* @param {function} listener The listener to be called when content
* completes.
*/
player.ima.addContentEndedListener = function(listener) {
contentEndedListeners.push(listener);
};
/**
* Pauses the ad.
*/
player.ima.pauseAd = function() {
if (adsActive && adPlaying) {
playPauseDiv.className = 'ima-paused';
adsManager.pause();
adPlaying = false;
}
};
/**
* Resumes the ad.
*/
player.ima.resumeAd = function() {
if (adsActive && !adPlaying) {
playPauseDiv.className = 'ima-playing';
adsManager.resume();
adPlaying = true;
}
};
/**
* Updates the current time of the video
*/
player.ima.updateCurrentTime = function() {
if (!contentPlayheadTracker.seeking) {
contentPlayheadTracker.currentTime = player.currentTime();
}
}
/**
* Detects when the user is seeking through a video.
* This is used to prevent mid-rolls from playing while a user is seeking.
*
* There *is* a seeking property of the HTML5 video element, but it's not
* properly implemented on all platforms (e.g. mobile safari), so we have to
* check ourselves to be sure.
*/
player.ima.checkForSeeking = function() {
var tempCurrentTime = player.currentTime();
var diff = (tempCurrentTime - contentPlayheadTracker.previousTime) * 1000;
if (Math.abs(diff) > seekCheckInterval + seekThreshold) {
contentPlayheadTracker.seeking = true;
} else {
contentPlayheadTracker.seeking = false;
}
contentPlayheadTracker.previousTime = player.currentTime();
}
/**
* Stores user-provided settings.
*/
var settings;
/**
* Video element playing content.
*/
var contentPlayer;
/**
* Video.js control bar.
*/
var vjsControls;
/**
* Div used as an ad container.
*/
var adContainerDiv;
/**
* Div used to display ad controls.
*/
var controlsDiv;
/**
* Div used to display ad countdown timer.
*/
var countdownDiv;
/**
* Div used to display add seek bar.
*/
var seekBarDiv;
/**
* Div used to display ad progress (in seek bar).
*/
var progressDiv;
/**
* Div used to display ad play/pause button.
*/
var playPauseDiv;
/**
* Div used to display ad mute button.
*/
var muteDiv;
/**
* Div used to display ad fullscreen button.
*/
var fullscreenDiv;
/**
* IMA SDK AdDisplayContainer.
*/
var adDisplayContainer;
/**
* True if the AdDisplayContainer has been initialized. False otherwise.
*/
var adDisplayContainerInitialized = false;
/**
* IMA SDK AdsLoader
*/
var adsLoader;
/**
* IMA SDK AdsManager
*/
var adsManager;
/**
* IMA SDK AdsRenderingSettings.
*/
var adsRenderingSettings = null;
/**
* Ad tag URL. Should return VAST, VMAP, or ad rules.
*/
var adTagUrl;
/**
* Current IMA SDK Ad.
*/
var currentAd;
/**
* Timer used to track content progress.
*/
var contentTrackingTimer;
/**
* Timer used to track ad progress.
*/
var adTrackingTimer;
/**
* True if ads are currently displayed, false otherwise.
* True regardless of ad pause state if an ad is currently being displayed.
*/
var adsActive = false;
/**
* True if ad is currently playing, false if ad is paused or ads are not
* currently displayed.
*/
var adPlaying = false;
/**
* True if the ad is muted, false otherwise.
*/
var adMuted = false;
/**
* True if our content video has completed, false otherwise.
*/
var contentComplete = false;
/**
* Interval (ms) on which to check if the user is seeking through the
* content.
*/
var seekCheckInterval = 1000;
/**
* Threshold by which to judge user seeking. We check every 1000 ms to see
* if the user is seeking. In order for us to decide that they are *not*
* seeking, the content video playhead must only change by 900-1100 ms
* between checks. Any greater change and we assume the user is seeking
* through the video.
*/
var seekThreshold = 100;
/**
* Stores data for the content playhead tracker.
*/
var contentPlayheadTracker = {
currentTime: 0,
previousTime: 0,
seeking: false,
duration: 0
};
/**
* Stores data for the ad playhead tracker.
*/
var adPlayheadTracker = {
currentTime: 0,
duration: 0,
isPod: false,
adPosition: 0,
totalAds: 0
};
/**
* Content ended listeners passed by the publisher to the plugin. Publishers
* should allow the plugin to handle content ended to ensure proper support
* of custom ad playback.
*/
var contentEndedListeners = [];
/**
* Local content ended listener for contentComplete.
*/
var localContentEndedListener = function() {
if (adsLoader && !contentComplete) {
adsLoader.contentComplete();
contentComplete = true;
}
for (var index in contentEndedListeners) {
contentEndedListeners[index]();
}
};
settings = extend({}, defaults, options || {});
// Currently this isn't used but I can see it being needed in the future, so
// to avoid implementation problems with later updates I'm requiring it.
if (!settings['id']) {
window.console.log('Error: must provide id of video.js div');
return;
}
contentPlayer = document.getElementById(settings['id'] + '_html5_api');
setInterval(player.ima.updateCurrentTime, seekCheckInterval);
setInterval(player.ima.checkForSeeking, seekCheckInterval);
player.on('ended', localContentEndedListener);
player.ads({debug: settings.debug});
adsRenderingSettings = new google.ima.AdsRenderingSettings();
adsRenderingSettings.restoreCustomPlaybackStateOnAdBreakComplete = true;
if (settings['adsRenderingSettings']) {
for (var setting in settings['adsRenderingSettings']) {
adsRenderingSettings[setting] =
settings['adsRenderingSettings'][setting];
}
}
if (settings['locale']) {
google.ima.settings.setLocale(settings['locale']);
}
player.ima.createAdContainer_();
adsLoader = new google.ima.AdsLoader(adDisplayContainer);
if (settings.locale) {
adsLoader.getSettings().setLocale(settings.locale);
}
adsLoader.addEventListener(
google.ima.AdsManagerLoadedEvent.Type.ADS_MANAGER_LOADED,
player.ima.onAdsManagerLoaded_,
false);
adsLoader.addEventListener(
google.ima.AdErrorEvent.Type.AD_ERROR,
player.ima.onAdsLoaderError_,
false);
if (!readyCallback) {
readyCallback = player.ima.start;
}
player.on('readyforpreroll', readyCallback);
player.on('fullscreenchange', player.ima.onFullscreenChange_);
player.on('volumechange', player.ima.onVolumeChange_);
};
vjs.plugin('ima', imaPlugin);
}(window.videojs));
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment