videojs-ima
Version:
[](https://travis-ci.org/googleads/videojs-ima)
624 lines (526 loc) • 16.8 kB
JavaScript
/**
* Copyright 2017 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
*/
/**
* Wraps the video.js player for the plugin.
*
* @param {Object} player Video.js player instance.
* @param {Object} adsPluginSettings Settings for the contrib-ads plugin.
* @param {Controller} controller Reference to the parent controller.
*/
const PlayerWrapper = function(player, adsPluginSettings, controller) {
/**
* Instance of the video.js player.
*/
this.vjsPlayer = player;
/**
* Plugin controller.
*/
this.controller = controller;
/**
* Timer used to track content progress.
*/
this.contentTrackingTimer = null;
/**
* True if our content video has completed, false otherwise.
*/
this.contentComplete = false;
/**
* Handle to interval that repeatedly updates current time.
*/
this.updateTimeIntervalHandle = null;
/**
* Interval (ms) to check for player resize for fluid support.
*/
this.updateTimeInterval = 1000;
/**
* Handle to interval that repeatedly checks for seeking.
*/
this.seekCheckIntervalHandle = null;
/**
* Interval (ms) on which to check if the user is seeking through the
* content.
*/
this.seekCheckInterval = 1000;
/**
* Handle to interval that repeatedly checks for player resize.
*/
this.resizeCheckIntervalHandle = null;
/**
* Interval (ms) to check for player resize for fluid support.
*/
this.resizeCheckInterval = 250;
/**
* 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.
*/
this.seekThreshold = 100;
/**
* 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.
*/
this.contentEndedListeners = [];
/**
* Stores the content source so we can re-populate it manually after a
* post-roll on iOS.
*/
this.contentSource = '';
/**
* Stores the content source type so we can re-populate it manually after a
* post-roll.
*/
this.contentSourceType = '';
/**
* Stores data for the content playhead tracker.
*/
this.contentPlayheadTracker = {
currentTime: 0,
previousTime: 0,
seeking: false,
duration: 0,
};
/**
* Player dimensions. Used in our resize check.
*/
this.vjsPlayerDimensions = {
width: this.getPlayerWidth(),
height: this.getPlayerHeight(),
};
/**
* Video.js control bar.
*/
this.vjsControls = this.vjsPlayer.getChild('controlBar');
/**
* Vanilla HTML5 video player underneath the video.js player.
*/
this.h5Player = null;
this.vjsPlayer.one('play', this.setUpPlayerIntervals.bind(this));
this.boundContentEndedListener = this.localContentEndedListener.bind(this);
this.vjsPlayer.on('contentended', this.boundContentEndedListener);
this.vjsPlayer.on('dispose', this.playerDisposedListener.bind(this));
this.vjsPlayer.on('readyforpreroll', this.onReadyForPreroll.bind(this));
this.vjsPlayer.ready(this.onPlayerReady.bind(this));
if (this.controller.getSettings().requestMode === 'onPlay') {
this.vjsPlayer.one('play',
this.controller.requestAds.bind(this.controller));
}
this.vjsPlayer.ads(adsPluginSettings);
};
/**
* Set up the intervals we use on the player.
*/
PlayerWrapper.prototype.setUpPlayerIntervals = function() {
this.updateTimeIntervalHandle =
setInterval(this.updateCurrentTime.bind(this), this.updateTimeInterval);
this.seekCheckIntervalHandle =
setInterval(this.checkForSeeking.bind(this), this.seekCheckInterval);
this.resizeCheckIntervalHandle =
setInterval(this.checkForResize.bind(this), this.resizeCheckInterval);
};
/**
* Updates the current time of the video
*/
PlayerWrapper.prototype.updateCurrentTime = function() {
if (!this.contentPlayheadTracker.seeking) {
this.contentPlayheadTracker.currentTime = this.vjsPlayer.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.
*/
PlayerWrapper.prototype.checkForSeeking = function() {
const tempCurrentTime = this.vjsPlayer.currentTime();
const diff =
(tempCurrentTime - this.contentPlayheadTracker.previousTime) * 1000;
if (Math.abs(diff) > this.seekCheckInterval + this.seekThreshold) {
this.contentPlayheadTracker.seeking = true;
} else {
this.contentPlayheadTracker.seeking = false;
}
this.contentPlayheadTracker.previousTime = this.vjsPlayer.currentTime();
};
/**
* Detects when the player is resized (for fluid support) and resizes the
* ads manager to match.
*/
PlayerWrapper.prototype.checkForResize = function() {
const currentWidth = this.getPlayerWidth();
const currentHeight = this.getPlayerHeight();
if (currentWidth != this.vjsPlayerDimensions.width ||
currentHeight != this.vjsPlayerDimensions.height) {
this.vjsPlayerDimensions.width = currentWidth;
this.vjsPlayerDimensions.height = currentHeight;
this.controller.onPlayerResize(currentWidth, currentHeight);
}
};
/**
* Local content ended listener for contentComplete.
*/
PlayerWrapper.prototype.localContentEndedListener = function() {
if (!this.contentComplete) {
this.contentComplete = true;
this.controller.onContentComplete();
}
for (let index in this.contentEndedListeners) {
if (typeof this.contentEndedListeners[index] === 'function') {
this.contentEndedListeners[index]();
}
}
clearInterval(this.updateTimeIntervalHandle);
clearInterval(this.seekCheckIntervalHandle);
clearInterval(this.resizeCheckIntervalHandle);
if (this.vjsPlayer.el()) {
this.vjsPlayer.one('play', this.setUpPlayerIntervals.bind(this));
}
};
/**
* Called when it's time to play a post-roll but we don't have one to play.
*/
PlayerWrapper.prototype.onNoPostroll = function() {
this.vjsPlayer.trigger('nopostroll');
};
/**
* Detects when the video.js player has been disposed.
*/
PlayerWrapper.prototype.playerDisposedListener = function() {
this.contentEndedListeners = [];
this.controller.onPlayerDisposed();
this.contentComplete = true;
this.vjsPlayer.off('contentended', this.boundContentEndedListener);
// Bug fix: https://github.com/googleads/videojs-ima/issues/306
if (this.vjsPlayer.ads.adTimeoutTimeout) {
clearTimeout(this.vjsPlayer.ads.adTimeoutTimeout);
}
const intervalsToClear = [
this.updateTimeIntervalHandle,
this.seekCheckIntervalHandle,
this.resizeCheckIntervalHandle];
for (let index in intervalsToClear) {
if (intervalsToClear[index]) {
clearInterval(intervalsToClear[index]);
}
}
};
/**
* Start ad playback, or content video playback in the absence of a
* pre-roll.
*/
PlayerWrapper.prototype.onReadyForPreroll = function() {
this.controller.onPlayerReadyForPreroll();
};
/**
* Called when the player fires its 'ready' event.
*/
PlayerWrapper.prototype.onPlayerReady = function() {
this.h5Player =
document.getElementById(
this.getPlayerId()).getElementsByClassName(
'vjs-tech')[0];
// Detect inline options
if (this.h5Player.hasAttribute('autoplay')) {
this.controller.setSetting('adWillAutoPlay', true);
}
// Sync ad volume with player volume.
this.onVolumeChange();
this.vjsPlayer.on('fullscreenchange', this.onFullscreenChange.bind(this));
this.vjsPlayer.on('volumechange', this.onVolumeChange.bind(this));
this.controller.onPlayerReady();
};
/**
* Listens for the video.js player to change its fullscreen status. This
* keeps the fullscreen-ness of the AdContainer in sync with the player.
*/
PlayerWrapper.prototype.onFullscreenChange = function() {
if (this.vjsPlayer.isFullscreen()) {
this.controller.onPlayerEnterFullscreen();
} else {
this.controller.onPlayerExitFullscreen();
}
};
/**
* 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.
*/
PlayerWrapper.prototype.onVolumeChange = function() {
const newVolume = this.vjsPlayer.muted() ? 0 : this.vjsPlayer.volume();
this.controller.onPlayerVolumeChanged(newVolume);
};
/**
* Inject the ad container div into the DOM.
*
* @param{HTMLElement} adContainerDiv The ad container div.
*/
PlayerWrapper.prototype.injectAdContainerDiv = function(adContainerDiv) {
this.vjsControls.el().parentNode.appendChild(adContainerDiv);
};
/**
* @return {Object} The content player.
*/
PlayerWrapper.prototype.getContentPlayer = function() {
return this.h5Player;
};
/**
* @return {number} The volume, 0-1.
*/
PlayerWrapper.prototype.getVolume = function() {
return this.vjsPlayer.muted() ? 0 : this.vjsPlayer.volume();
};
/**
* Set the volume of the player. 0-1.
*
* @param {number} volume The new volume.
*/
PlayerWrapper.prototype.setVolume = function(volume) {
this.vjsPlayer.volume(volume);
if (volume == 0) {
this.vjsPlayer.muted(true);
} else {
this.vjsPlayer.muted(false);
}
};
/**
* Ummute the player.
*/
PlayerWrapper.prototype.unmute = function() {
this.vjsPlayer.muted(false);
};
/**
* Mute the player.
*/
PlayerWrapper.prototype.mute = function() {
this.vjsPlayer.muted(true);
};
/**
* Play the video.
*/
PlayerWrapper.prototype.play = function() {
this.vjsPlayer.play();
};
/**
* Get the player width.
*
* @return {number} The player's width.
*/
PlayerWrapper.prototype.getPlayerWidth = function() {
let width = (getComputedStyle(this.vjsPlayer.el()) || {}).width;
if (!width || parseFloat(width) === 0) {
width = (this.vjsPlayer.el().getBoundingClientRect() || {}).width;
}
return parseFloat(width) || this.vjsPlayer.width();
};
/**
* Get the player height.
*
* @return {number} The player's height.
*/
PlayerWrapper.prototype.getPlayerHeight = function() {
let height = (getComputedStyle(this.vjsPlayer.el()) || {}).height;
if (!height || parseFloat(height) === 0) {
height = (this.vjsPlayer.el().getBoundingClientRect() || {}).height;
}
return parseFloat(height) || this.vjsPlayer.height();
};
/**
* @return {Object} The vjs player's options object.
*/
PlayerWrapper.prototype.getPlayerOptions = function() {
return this.vjsPlayer.options_;
};
/**
* Returns the instance of the player id.
* @return {string} The player id.
*/
PlayerWrapper.prototype.getPlayerId = function() {
return this.vjsPlayer.id();
};
/**
* Toggle fullscreen state.
*/
PlayerWrapper.prototype.toggleFullscreen = function() {
if (this.vjsPlayer.isFullscreen()) {
this.vjsPlayer.exitFullscreen();
} else {
this.vjsPlayer.requestFullscreen();
}
};
/**
* Returns the content playhead tracker.
*
* @return {Object} The content playhead tracker.
*/
PlayerWrapper.prototype.getContentPlayheadTracker = function() {
return this.contentPlayheadTracker;
};
/**
* Handles ad errors.
*
* @param {Object} adErrorEvent The ad error event thrown by the IMA SDK.
*/
PlayerWrapper.prototype.onAdError = function(adErrorEvent) {
this.vjsControls.show();
const errorMessage =
adErrorEvent.getError !== undefined ?
adErrorEvent.getError() : adErrorEvent.stack;
this.vjsPlayer.trigger({type: 'adserror', data: {
AdError: errorMessage,
AdErrorEvent: adErrorEvent,
}});
};
/**
* Handles ad log messages.
* @param {google.ima.AdEvent} adEvent The AdEvent thrown by the IMA SDK.
*/
PlayerWrapper.prototype.onAdLog = function(adEvent) {
const adData = adEvent.getAdData();
const errorMessage =
adData['adError'] !== undefined ?
adData['adError'].getMessage() : undefined;
this.vjsPlayer.trigger({type: 'adslog', data: {
AdError: errorMessage,
AdEvent: adEvent,
}});
};
/**
* Handles ad break starting.
*/
PlayerWrapper.prototype.onAdBreakStart = function() {
this.contentSource = this.vjsPlayer.currentSrc();
this.contentSourceType = this.vjsPlayer.currentType();
this.vjsPlayer.off('contentended', this.boundContentEndedListener);
this.vjsPlayer.ads.startLinearAdMode();
this.vjsControls.hide();
this.vjsPlayer.pause();
};
/**
* Handles ad break ending.
*/
PlayerWrapper.prototype.onAdBreakEnd = function() {
this.vjsPlayer.on('contentended', this.boundContentEndedListener);
if (this.vjsPlayer.ads.inAdBreak()) {
this.vjsPlayer.ads.endLinearAdMode();
}
this.vjsControls.show();
};
/**
* Handles an individual ad start.
*/
PlayerWrapper.prototype.onAdStart = function() {
this.vjsPlayer.trigger('ads-ad-started');
};
/**
* Handles when all ads have finished playing.
*/
PlayerWrapper.prototype.onAllAdsCompleted = function() {
if (this.contentComplete == true) {
// The null check on this.contentSource was added to fix
// an error when the post-roll was an empty VAST tag.
if (this.contentSource &&
this.vjsPlayer.currentSrc() != this.contentSource) {
this.vjsPlayer.src({
src: this.contentSource,
type: this.contentSourceType,
});
}
this.controller.onContentAndAdsCompleted();
}
};
/**
* Triggers adsready for contrib-ads.
*/
PlayerWrapper.prototype.onAdsReady = function() {
this.vjsPlayer.trigger('adsready');
};
/**
* Changes the player source.
* @param {?string} contentSrc The URI for the content to be played. Leave
* blank to use the existing content.
*/
PlayerWrapper.prototype.changeSource = function(contentSrc) {
// Only try to pause the player when initialised with a source already
if (this.vjsPlayer.currentSrc()) {
this.vjsPlayer.currentTime(0);
this.vjsPlayer.pause();
}
if (contentSrc) {
this.vjsPlayer.src(contentSrc);
}
this.vjsPlayer.one('loadedmetadata', this.seekContentToZero.bind(this));
};
/**
* 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.
*/
PlayerWrapper.prototype.seekContentToZero = function() {
this.vjsPlayer.currentTime(0);
};
/**
* Triggers an event on the VJS player
* @param {string} name The event name.
* @param {Object} data The event data.
*/
PlayerWrapper.prototype.triggerPlayerEvent = function(name, data) {
this.vjsPlayer.trigger(name, data);
};
/**
* Listener JSDoc for ESLint. This listener can be passed to
* addContentEndedListener.
* @callback listener
*/
/**
* Adds a listener for the 'contentended' event of the video player. This should
* be used instead of setting an 'contentended' listener directly to ensure that
* the ima can do proper cleanup of the SDK before other event listeners are
* called.
* @param {listener} listener The listener to be called when content
* completes.
*/
PlayerWrapper.prototype.addContentEndedListener = function(listener) {
this.contentEndedListeners.push(listener);
};
/**
* Reset the player.
*/
PlayerWrapper.prototype.reset = function() {
// Attempts to remove the contentEndedListener before adding it.
// This is to prevent an error where an erroring video caused multiple
// contentEndedListeners to be added.
this.vjsPlayer.off('contentended', this.boundContentEndedListener);
this.vjsPlayer.on('contentended', this.boundContentEndedListener);
this.vjsControls.show();
if (this.vjsPlayer.ads.inAdBreak()) {
this.vjsPlayer.ads.endLinearAdMode();
}
// Reset the content time we give the SDK. Fixes an issue where requesting
// VMAP followed by VMAP would play the second mid-rolls as pre-rolls if
// the first playthrough of the video passed the second response's
// mid-roll time.
this.contentPlayheadTracker.currentTime = 0;
this.contentComplete = false;
};
export default PlayerWrapper;