UNPKG

cloudinary-video-player

Version:

Cloudinary Video Player

1,515 lines (1,375 loc) 60.2 kB
/* eslint-disable */ /** * 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(factory) { if (typeof define === 'function' && define['amd']) { define(['video.js', 'videojs-contrib-ads'], function(videojs){ factory(window, document, videojs.default) }); } else if (typeof exports === 'object' && typeof module === 'object') { var vjs = require('video.js'); require('videojs-contrib-ads'); factory(window, document, vjs); } else { factory(window, document, videojs); } })(function(window, document, videojs) { "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; }; var ima_defaults = { debug: false, timeout: 5000, prerollTimeout: 100, adLabel: 'Advertisement', showControlsForAds: true, showControlsForJSAds: true, adsRenderingSettings: { uiElements: [], }, }; var eventTypes = (videojs.browser.IS_ANDROID || videojs.browser.IS_IOS) ? { click: 'touchend', mousedown: 'touchstart', mouseup: 'touchend', mousemove: 'touchmove' } : { click: 'click', mousedown: 'mousedown', mouseup: 'mouseup', mousemove: 'mousemove' }; var init = function(options, readyCallback) { this.ima = new ImaPlugin(this, options, readyCallback); }; var ImaPlugin = function(player, options, readyCallback) { this.player = player; /** * Assigns the unique id and class names to the given element as well as the style class * @param element * @param controlName * @private */ var assignControlAttributes_ = function(element, controlName) { element.id = this.controlPrefix + controlName; element.className = this.controlPrefix + controlName + ' ' + controlName; }.bind(this); /** * Returns a regular expression to test a string for the given className * @param className * @returns {RegExp} * @private */ var getClassRegexp_ = function(className){ // Matches on // (beginning of string OR NOT word char) // classname // (negative lookahead word char OR end of string) return new RegExp('(^|[^A-Za-z-])' + className + '((?![A-Za-z-])|$)', 'gi'); }; /** * Adds a class to the given element if it doesn't already have the class * @param element * @param classToAdd * @private */ var addClass_ = function(element, classToAdd){ if(getClassRegexp_(classToAdd).test(element.className)){ return element; } return element.className = element.className.trim() + ' ' + classToAdd; }; /** * Removes a class from the given element if it has the given class * @param element * @param classToRemove * @private */ var removeClass_ = function(element, classToRemove){ var classRegexp = getClassRegexp_(classToRemove); if(!classRegexp.test(element.className)){ return element; } return element.className = element.className.trim().replace(classRegexp, ''); }; /** * Creates the ad container passed to the IMA SDK. * @private */ var createAdContainer_ = function() { // The adContainerDiv is the DOM of the element that will house // the ads and ad controls. this.vjsControls = this.player.getChild('controlBar'); this.adContainerDiv = this.vjsControls.el().parentNode.appendChild( document.createElement('div')); assignControlAttributes_(this.adContainerDiv, 'ima-ad-container'); this.adContainerDiv.style.position = "absolute"; this.adContainerDiv.addEventListener( 'mouseenter', showAdControls_, false); this.adContainerDiv.addEventListener( 'mouseleave', hideAdControls_, false); createControls_(); this.adDisplayContainer = new google.ima.AdDisplayContainer(this.adContainerDiv, this.contentPlayer); this.showAdContainer(!this.settings.manual); }.bind(this); /** * Creates the controls for the ad. * @private */ var createControls_ = function() { this.controlsDiv = document.createElement('div'); assignControlAttributes_(this.controlsDiv, 'ima-controls-div'); this.controlsDiv.style.width = '100%'; this.countdownDiv = document.createElement('div'); assignControlAttributes_(this.countdownDiv, 'ima-countdown-div'); this.countdownDiv.innerHTML = this.settings.adLabel; this.countdownDiv.style.display = this.showCountdown ? '' : 'none'; this.seekBarDiv = document.createElement('div'); assignControlAttributes_(this.seekBarDiv, 'ima-seek-bar-div'); this.seekBarDiv.style.width = '100%'; this.progressDiv = document.createElement('div'); assignControlAttributes_(this.progressDiv, 'ima-progress-div'); this.playPauseDiv = document.createElement('div'); assignControlAttributes_(this.playPauseDiv, 'ima-play-pause-div'); addClass_(this.playPauseDiv, 'ima-playing'); this.playPauseDiv.addEventListener( eventTypes.click, onAdPlayPauseClick_, false); this.muteDiv = document.createElement('div'); assignControlAttributes_(this.muteDiv, 'ima-mute-div'); addClass_(this.muteDiv, 'ima-non-muted'); this.muteDiv.addEventListener( eventTypes.click, onAdMuteClick_, false); this.sliderDiv = document.createElement('div'); assignControlAttributes_(this.sliderDiv, 'ima-slider-div'); this.sliderDiv.addEventListener( eventTypes.mousedown, onAdVolumeSliderMouseDown_, false); this.sliderLevelDiv = document.createElement('div'); assignControlAttributes_(this.sliderLevelDiv, 'ima-slider-level-div'); this.fullscreenDiv = document.createElement('div'); assignControlAttributes_(this.fullscreenDiv, 'ima-fullscreen-div'); addClass_(this.fullscreenDiv, 'ima-non-fullscreen'); this.fullscreenDiv.addEventListener( eventTypes.click, onAdFullscreenClick_, false); this.adContainerDiv.appendChild(this.controlsDiv); this.controlsDiv.appendChild(this.seekBarDiv); this.controlsDiv.appendChild(this.playPauseDiv); this.controlsDiv.appendChild(this.muteDiv); this.controlsDiv.appendChild(this.sliderDiv); this.controlsDiv.appendChild(this.fullscreenDiv); this.seekBarDiv.appendChild(this.progressDiv); this.sliderDiv.appendChild(this.sliderLevelDiv); if (this.settings.vjsControls) { this.initVjsControls(); this.controlsDiv.style.display = 'none'; this.vjsControls.el().appendChild(this.countdownDiv); } else { this.controlsDiv.appendChild(this.countdownDiv); } }.bind(this); this.showAdContainer = function(show) { this.adContainerDiv.style.display = show ? 'block' : 'none'; this.player.toggleClass('vjs-ima-ad', show); }.bind(this); /** * Initializes the AdDisplayContainer. On mobile, this must be done as a * result of user action. */ this.initializeAdDisplayContainer = function() { this.adDisplayContainerInitialized = true; this.adDisplayContainer.initialize(); }.bind(this); /** * Creates the AdsRequest and request ads through the AdsLoader. */ this.requestAds = function() { if (!this.adDisplayContainerInitialized) { this.adDisplayContainer.initialize(); } var adsRequest = new google.ima.AdsRequest(); if (this.settings.adTagUrl) { adsRequest.adTagUrl = this.settings.adTagUrl; } else { adsRequest.adsResponse = this.settings.adsResponse; } if (this.settings.forceNonLinearFullSlot) { adsRequest.forceNonLinearFullSlot = true; } adsRequest.linearAdSlotWidth = this.getPlayerWidth(); adsRequest.linearAdSlotHeight = this.getPlayerHeight(); adsRequest.nonLinearAdSlotWidth = this.settings.nonLinearWidth || this.getPlayerWidth(); adsRequest.nonLinearAdSlotHeight = this.settings.nonLinearHeight || (this.getPlayerHeight() / 3); adsRequest.vastLoadTimeout = Math.max(this.settings.prerollTimeout, this.settings.postrollTimeout); this.adsLoader.requestAds(adsRequest); }.bind(this); /** * Listener for the ADS_MANAGER_LOADED event. Creates the AdsManager, * sets up event listeners, and triggers the 'adsready' event for * videojs-ads-contrib. * @private */ var onAdsManagerLoaded_ = function(adsManagerLoadedEvent) { this.adsManager = adsManagerLoadedEvent.getAdsManager( this.contentPlayheadTracker, this.adsRenderingSettings); this.adsManager.addEventListener( google.ima.AdErrorEvent.Type.AD_ERROR, onAdError_); this.adsManager.addEventListener( google.ima.AdEvent.Type.AD_BREAK_READY, onAdBreakReady_); this.adsManager.addEventListener( google.ima.AdEvent.Type.CONTENT_PAUSE_REQUESTED, this.onContentPauseRequested_); this.adsManager.addEventListener( google.ima.AdEvent.Type.CONTENT_RESUME_REQUESTED, this.onContentResumeRequested_); this.adsManager.addEventListener( google.ima.AdEvent.Type.ALL_ADS_COMPLETED, onAllAdsCompleted_); this.adsManager.addEventListener( google.ima.AdEvent.Type.LOADED, onAdLoaded_); this.adsManager.addEventListener( google.ima.AdEvent.Type.STARTED, onAdStarted_); this.adsManager.addEventListener( google.ima.AdEvent.Type.CLICK, onAdPlayPauseClick_); this.adsManager.addEventListener( google.ima.AdEvent.Type.COMPLETE, this.onAdComplete_); this.adsManager.addEventListener( google.ima.AdEvent.Type.SKIPPED, this.onAdComplete_); this.adsManager.addEventListener( google.ima.AdEvent.Type.PAUSED, this.onAdPaused_); this.adsManager.addEventListener( google.ima.AdEvent.Type.RESUMED, this.onAdResumed_); var eventsMap = { 'load': google.ima.AdEvent.Type.LOADED, 'ad-started': google.ima.AdEvent.Type.STARTED, 'click': google.ima.AdEvent.Type.CLICK, 'ad-ended': google.ima.AdEvent.Type.COMPLETE, 'ad-skipped': google.ima.AdEvent.Type.SKIPPED, 'first-quartile': google.ima.AdEvent.Type.FIRST_QUARTILE, 'midpoint': google.ima.AdEvent.Type.MIDPOINT, 'third-quartile': google.ima.AdEvent.Type.THIRD_QUARTILE, 'impression': google.ima.AdEvent.Type.IMPRESSION, 'pause': google.ima.AdEvent.Type.PAUSED, 'play': google.ima.AdEvent.Type.RESUMED, 'mute': google.ima.AdEvent.Type.VOLUME_MUTED, 'allpods-completed': google.ima.AdEvent.Type.ALL_ADS_COMPLETED }; Object.keys(eventsMap).forEach(function(event){ this.adsManager.addEventListener(eventsMap[event], function(){ this.player.trigger('ads-'+event); }.bind(this)); }.bind(this)); setAdMuted(this.player.muted()); if (!this.autoPlayAdBreaks) { try { var initWidth = this.getPlayerWidth(); var initHeight = this.getPlayerHeight(); this.adsManagerDimensions.width = initWidth; this.adsManagerDimensions.height = initHeight; this.adsManager.init( initWidth, initHeight, google.ima.ViewMode.NORMAL); this.adsManager.setVolume(this.player.muted() ? 0 : this.player.volume()); } catch (adError) { onAdError_(adError); } } var cuepoints = this.adsManager.getCuePoints(); var foundpreroll = !cuepoints.length; // no playlist, just preroll var foundpostroll = false;; cuepoints.forEach(function(offset){ if (!offset) foundpreroll = true; else if (offset==-1) foundpostroll = true; }); if (!foundpreroll) this.player.trigger('nopreroll'); if (!foundpostroll) this.player.trigger('nopostroll'); if (cuepoints.length) this.player.trigger('ads-cuepoints', cuepoints); this.player.trigger('adsready'); }.bind(this); /** * DEPRECATED: Use startFromReadyCallback * Start ad playback, or content video playback in the absence of a * pre-roll. */ this.start = function() { window.console.log( 'WARNING: player.ima.start is deprecated. Use ' + 'player.ima.startFromReadyCallback instead.'); }; /** * Start ad playback, or content video playback in the absence of a * pre-roll. **NOTE**: This method only needs to be called if you provide * your own readyCallback as the second parameter to player.ima(). If you * only provide options and do not provide your own readyCallback, * **DO NOT** call this method. If you do provide your own readyCallback, * you should call this method in the last line of that callback. For more * info, see this method's usage in our advanced and playlist examples. */ this.startFromReadyCallback = function() { if (this.autoPlayAdBreaks && this.adsManager) { try { this.adsManager.init( this.getPlayerWidth(), this.getPlayerHeight(), google.ima.ViewMode.NORMAL); this.adsManager.setVolume(this.player.muted() ? 0 : this.player.volume()); this.adsManager.start(); } catch (adError) { onAdError_(adError); } } }.bind(this); /** * 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 * @private */ var onAdsLoaderError_ = function(event) { console.log('AdsLoader error: ' + event.getError()); this.showAdContainer(false); if (this.adsManager) { this.adsManager.destroy(); } this.player.trigger({type: 'adserror', data: { AdError: event.getError(), AdErrorEvent: event }}); }.bind(this); /** * Listener for errors thrown by the AdsManager. * @param {google.ima.AdErrorEvent} adErrorEvent The error event thrown by * the AdsManager. * @private */ var onAdError_ = function(adErrorEvent) { var errorMessage = adErrorEvent.getError !== undefined ? adErrorEvent.getError() : adErrorEvent.stack; console.log('Ad error: ' + errorMessage); this.adsActive = false; this.adPlaying = false; this.restoreLoop(); this.vjsControls.show(); this.adsManager.destroy(); this.showAdContainer(false); this.updateVjsControls(); this.player.trigger({ type: 'adserror', data: { AdError: errorMessage, AdErrorEvent: adErrorEvent }}); }.bind(this); /** * Listener for AD_BREAK_READY. Passes event on to publisher's listener. * @param {google.ima.AdEvent} adEvent AdEvent thrown by the AdsManager. * @private */ var onAdBreakReady_ = function(adEvent) { this.adBreakReadyListener(adEvent); }.bind(this); /** * Called by publishers in manual ad break playback mode to start an ad * break. */ this.playAdBreak = function() { if (!this.autoPlayAdBreaks) { this.adsManager.start(); } }.bind(this); this.resetLoop = function() { this.contentLoop = this.contentPlayer && this.contentPlayer.loop; if (this.contentLoop) { this.contentPlayer.loop = false; } }.bind(this); this.restoreLoop = function() { if (this.contentLoop) { this.contentPlayer.loop = true; this.contentLoop = false; } }.bind(this); /** * Pauses the content video and displays the ad container so ads can play. * @param {google.ima.AdEvent} adEvent The AdEvent thrown by the AdsManager. * @private */ this.onContentPauseRequested_ = function(adEvent) { this.contentSource = this.player.currentSrc(); this.resetLoop(); this.player.off('contentended', this.localContentEndedListener); this.player.ads.startLinearAdMode(); this.showAdContainer(true); var contentType = adEvent.getAd().getContentType(); if (!this.settings.vjsControls || !this.settings.showControlsForAds){ if (!this.settings.showControlsForAds || ((contentType === 'application/javascript') && !this.settings.showControlsForJSAds)) { this.controlsDiv.style.display = 'none'; } else { this.controlsDiv.style.display = 'block'; } this.vjsControls.hide(); } showPlayButton(); this.player.pause(); this.adsActive = true; this.adPlaying = true; this.updateVjsControls(); }.bind(this); /** * Resumes content video and hides the ad container. * @param {google.ima.AdEvent} adEvent The AdEvent thrown by the AdsManager. * @private */ this.onContentResumeRequested_ = function(adEvent) { this.contentResumeTimer = clearTimeout(this.contentResumeTimer); this.restoreLoop(); this.adsActive = false; this.adPlaying = false; this.player.on('contentended', this.localContentEndedListener); if (this.currentAd == null || // hide for post-roll only playlist this.currentAd.isLinear()) { // don't hide for non-linear ads this.showAdContainer(false); } this.vjsControls.show(); this.player.ads.endLinearAdMode(); this.countdownDiv.innerHTML = ''; this.updateVjsControls(); }.bind(this); /** * Records that ads have completed and calls contentAndAdsEndedListeners * if content is also complete. * @param {google.ima.AdEvent} adEvent The AdEvent thrown by the AdsManager. * @private */ var onAllAdsCompleted_ = function(adEvent) { this.allAdsCompleted = true; this.showAdContainer(false); if (this.contentComplete == true) { if (this.contentPlayer.src && !/^blob:/.test(this.contentPlayer.src) && this.contentSource && this.contentPlayer.src != this.contentSource) { this.player.src(this.contentSource); } this.player.trigger('') for (var index in this.contentAndAdsEndedListeners) { this.contentAndAdsEndedListeners[index](); } } }.bind(this); /** * Starts the content video when a non-linear ad is loaded. * @param {google.ima.AdEvent} adEvent The AdEvent thrown by the AdsManager. * @private */ var onAdLoaded_ = function(adEvent) { if (!adEvent.getAd().isLinear() && !this.player.ended()) { this.player.ads.endLinearAdMode(); this.player.play(); } }.bind(this); /** * 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. * @private */ var onAdStarted_ = function(adEvent) { this.currentAd = adEvent.getAd(); if (this.currentAd.isLinear()) { this.adTrackingTimer = setInterval( onAdPlayheadTrackerInterval_, 250); // Don't bump container when controls are shown removeClass_(this.adContainerDiv, 'bumpable-ima-ad-container'); } else { // Bump container when controls are shown addClass_(this.adContainerDiv, 'bumpable-ima-ad-container'); this.player.addClass('vjs-ima-non-linear'); this.showAdContainer(true); } }.bind(this); /** * Clears the interval timer for current ad time when an ad completes. * @param {google.ima.AdEvent} adEvent The AdEvent thrown by the AdsManager. * @private */ this.onAdComplete_ = function(adEvent) { if (this.currentAd.isLinear()) { clearInterval(this.adTrackingTimer); var pod = this.currentAd.getAdPodInfo(); if (pod && pod.getAdPosition() < pod.getTotalAds()) { this.player.trigger('ads-pod-ended') return; } // this is the final ad so we excpect ima sdk to trigger // CONTENT_RESUME_REQUESTED, but for some reason it isn't triggered // reliably on iOS, so we fake it this.contentResumeTimer = setTimeout(function(){ this.onContentResumeRequested_(null); }.bind(this), 1000); } else { this.player.removeClass('vjs-ima-non-linear'); } }.bind(this); this.onAdPaused_ = function(adEvent) { showPauseButton(); this.adPlaying = false; }.bind(this); this.onAdResumed_ = function(adEvent) { showPlayButton(); this.adPlaying = true; }.bind(this); var formatTime = function(time) { var m = Math.floor(time / 60); var s = Math.floor(time % 60); if (s.toString().length < 2) { s = '0' + s; } return m + ':' + s; }; /** * Gets the current time and duration of the ad and calls the method to * update the ad UI. * @private */ var onAdPlayheadTrackerInterval_ = function() { var remainingTime = this.adsManager.getRemainingTime(); var duration = this.currentAd.getDuration(); var currentTime = duration - remainingTime; currentTime = currentTime > 0 ? currentTime : 0; var isPod = false; var totalAds = 0; var adPosition; if (this.currentAd.getAdPodInfo()) { isPod = true; adPosition = this.currentAd.getAdPodInfo().getAdPosition(); totalAds = this.currentAd.getAdPodInfo().getTotalAds(); } // Update countdown timer data var podCount = ': '; if (isPod && (totalAds > 1)) { podCount = ' (' + adPosition + ' of ' + totalAds + '): '; } this.countdownDiv.innerHTML = this.settings.adLabel + podCount + formatTime(remainingTime); // Update UI var playProgressRatio = currentTime / duration; var playProgressPercent = playProgressRatio * 100; this.progressDiv.style.width = playProgressPercent + '%'; this.updateVjsControls(); }.bind(this); this.getPlayerWidth = function() { var retVal = parseInt(getComputedStyle(this.player.el()).width, 10) || this.player.width(); return retVal; }.bind(this); this.getPlayerHeight = function() { var retVal = parseInt(getComputedStyle(this.player.el()).height, 10) || this.player.height(); return retVal; }.bind(this); /** * Hides the ad controls on mouseout. * @private */ var hideAdControls_ = function() { this.controlsDiv.style.height = '14px'; this.playPauseDiv.style.display = 'none'; this.muteDiv.style.display = 'none'; this.sliderDiv.style.display = 'none'; this.fullscreenDiv.style.display = 'none'; }.bind(this); /** * Shows ad controls on mouseover. * @private */ var showAdControls_ = function() { this.controlsDiv.style.height = '37px'; this.playPauseDiv.style.display = 'block'; this.muteDiv.style.display = 'block'; this.sliderDiv.style.display = 'block'; this.fullscreenDiv.style.display = 'block'; }.bind(this); /** * Show pause and hide play button */ var showPauseButton = function() { addClass_(this.playPauseDiv, 'ima-paused'); removeClass_(this.playPauseDiv, 'ima-playing'); }.bind(this); /** * Show play and hide pause button */ var showPlayButton = function() { addClass_(this.playPauseDiv, 'ima-playing'); removeClass_(this.playPauseDiv, 'ima-paused'); }.bind(this); /** * Listener for clicks on the play/pause button during ad playback. * @private */ var onAdPlayPauseClick_ = function() { if (this.adPlaying) { showPauseButton(); this.adsManager.pause(); this.adPlaying = false; } else { showPlayButton(); this.adsManager.resume(); this.adPlaying = true; } }.bind(this); /** * Listener for clicks on the mute button during ad playback. * @private */ var onAdMuteClick_ = function() { setAdMuted(!this.adMuted); }.bind(this); /* Listener for mouse down events during ad playback. Used for volume. * @private */ var onAdVolumeSliderMouseDown_ = function() { document.addEventListener(eventTypes.mouseup, onMouseUp_, false); document.addEventListener(eventTypes.mousemove, onMouseMove_, false); }; /* Mouse movement listener used for volume slider. * @private */ var onMouseMove_ = function(event) { setVolumeSlider_(event); }; /* Mouse release listener used for volume slider. * @private */ var onMouseUp_ = function(event) { setVolumeSlider_(event); document.removeEventListener(eventTypes.mousemove, onMouseMove_); document.removeEventListener(eventTypes.mouseup, onMouseUp_); }; /* Utility function to set volume and associated UI * @private */ var setVolumeSlider_ = function(event) { var clientX = event.changedTouches ? event.changedTouches[0].clientX : event.clientX; var percent = (clientX - this.sliderDiv.getBoundingClientRect().left) / this.sliderDiv.offsetWidth; percent *= 100; //Bounds value 0-100 if mouse is outside slider region. percent = Math.min(Math.max(percent, 0), 100); this.sliderLevelDiv.style.width = percent + "%"; this.player.volume(percent / 100); //0-1 this.adsManager.setVolume(percent / 100); if (this.player.volume() == 0) { addClass_(this.muteDiv, 'ima-muted'); removeClass_(this.muteDiv, 'ima-non-muted'); this.player.muted(true); this.adMuted = true; } else { addClass_(this.muteDiv, 'ima-non-muted'); removeClass_(this.muteDiv, 'ima-muted'); this.player.muted(false); this.adMuted = false; } }.bind(this); /** * Listener for clicks on the fullscreen button during ad playback. * @private */ var onAdFullscreenClick_ = function() { if (this.player.isFullscreen()) { this.player.exitFullscreen(); } else { this.player.requestFullscreen(); } }.bind(this); /** * Listens for the video.js player to change its fullscreen status. This * keeps the fullscreen-ness of the AdContainer in sync with the player. * @private */ var onFullscreenChange_ = function() { if (this.player.isFullscreen()) { addClass_(this.fullscreenDiv, 'ima-fullscreen'); removeClass_(this.fullscreenDiv, 'ima-non-fullscreen'); if (this.adsManager) { this.adsManager.resize( window.screen.width, window.screen.height, google.ima.ViewMode.FULLSCREEN); } } else { addClass_(this.fullscreenDiv, 'ima-non-fullscreen'); removeClass_(this.fullscreenDiv, 'ima-fullscreen'); if (this.adsManager) { this.adsManager.resize( this.getPlayerWidth(), this.getPlayerHeight(), google.ima.ViewMode.NORMAL); } } }.bind(this); /** * 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 * @private */ var onVolumeChange_ = function() { var newVolume = this.player.muted() ? 0 : this.player.volume(); if (this.adsManager) { this.adsManager.setVolume(newVolume); } // Update UI if (newVolume == 0) { this.adMuted = true; addClass_(this.muteDiv, 'ima-muted'); removeClass_(this.muteDiv, 'ima-non-muted'); this.sliderLevelDiv.style.width = '0%'; } else { this.adMuted = false; addClass_(this.muteDiv, 'ima-non-muted'); removeClass_(this.muteDiv, 'ima-muted'); this.sliderLevelDiv.style.width = newVolume * 100 + '%'; } }.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. * @private */ var seekContentToZero_ = function() { this.player.off('loadedmetadata', seekContentToZero_); this.player.currentTime(0); }.bind(this); /** * 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. * @private */ var playContentFromZero_ = function() { this.player.off('loadedmetadata', playContentFromZero_); this.player.currentTime(0); this.player.play(); }.bind(this); /** * 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. * @private */ var resetIMA_ = function() { this.adsActive = false; this.adPlaying = false; this.restoreLoop(); this.player.on('contentended', this.localContentEndedListener); if (this.currentAd && this.currentAd.isLinear()) { this.showAdContainer(false); } this.vjsControls.show(); this.player.ads.endLinearAdMode(); this.contentPlayheadTracker.currentTime = 0; this.countdownDiv.innerHTML = ''; this.updateVjsControls(); if (this.adTrackingTimer) { // If this is called while an ad is playing, stop trying to get that // ad's current time. clearInterval(this.adTrackingTimer); } if (this.adsManager) { this.adsManager.destroy(); this.adsManager = null; } if (this.adsLoader && !this.contentComplete) { this.adsLoader.contentComplete(); } this.contentComplete = false; this.allAdsCompleted = false; }.bind(this); /** * 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. */ this.addEventListener = function(event, callback) { if (this.adsManager) { this.adsManager.addEventListener(event, callback); } }.bind(this); /** * Returns the instance of the AdsManager. * @return {google.ima.AdsManager} The AdsManager being used by the plugin. */ this.getAdsManager = function() { return this.adsManager; }.bind(this); /** * DEPRECATED: Use setContentWithAdTag. * 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. Leave * blank to use the existing content. * @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. */ this.setContent = function(contentSrc, adTag, playOnLoad) { window.console.log( 'WARNING: player.ima.setContent is deprecated. Use ' + 'player.ima.setContentWithAdTag instead.'); this.setContentWithAdTag(contentSrc, adTag, playOnLoad); }.bind(this); /** * 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. Leave * blank to use the existing content. * @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. */ this.setContentWithAdTag = function(contentSrc, adTag, playOnLoad) { resetIMA_(); this.settings.adTagUrl = adTag ? adTag : this.settings.adTagUrl; changeSource_(contentSrc, playOnLoad); }.bind(this); /** * Sets the content of the video player. You should use this method instead * of setting the content src directly to ensure the proper ads response is * used when the video content is loaded. * @param {?string} contentSrc The URI for the content to be played. Leave * blank to use the existing content. * @param {?string} adsResponse The ads response to be requested when the * content loads. Leave blank to use the existing ads response. * @param {?boolean} playOnLoad True to play the content once it has loaded, * false to only load the content but not start playback. */ this.setContentWithAdsResponse = function(contentSrc, adsResponse, playOnLoad) { resetIMA_(); this.settings.adsResponse = adsResponse ? adsResponse : this.settings.adsResponse; changeSource_(contentSrc, playOnLoad); }.bind(this); /** * Plays an ad immediately * @param {?string} adTag The ad tag to be requested. * Leave blank to use the existing ad tag. */ this.playAd = function(adTag) { resetIMA_(); this.settings.adTagUrl = adTag ? adTag : this.settings.adTagUrl; // this.showAdContainer(true); // this.vjsControls.hide(); this.requestAds(); }.bind(this); /** * Changes the player source. * @param {?string} contentSrc The URI for the content to be played. Leave * blank to use the existing content. * @param {?boolean} playOnLoad True to play the content once it has loaded, * false to only load the content but not start playback. * @private */ var changeSource_ = function(contentSrc, playOnLoad) { // Only try to pause the player when initialised with a source already if (!!this.player.currentSrc()) { this.player.currentTime(0); this.player.pause(); } if (contentSrc) { this.player.src(contentSrc); } if (playOnLoad) { this.player.on('loadedmetadata', playContentFromZero_); } else { this.player.on('loadedmetadata', seekContentToZero_); } }.bind(this); var setAdMuted = function(mute) { if (mute) { addClass_(this.muteDiv, 'ima-muted'); removeClass_(this.muteDiv, 'ima-non-muted'); this.adsManager.setVolume(0); // Bubble down to content player this.player.muted(true); this.adMuted = true; this.sliderLevelDiv.style.width = "0%"; } else { addClass_(this.muteDiv, 'ima-non-muted'); removeClass_(this.muteDiv, 'ima-muted'); this.adsManager.setVolume(this.player.volume()); // Bubble down to content player this.player.muted(false); this.adMuted = false; this.sliderLevelDiv.style.width = this.player.volume() * 100 + "%"; } }.bind(this); /** * 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 {function} listener The listener to be called when content completes. */ this.addContentEndedListener = function(listener) { this.contentEndedListeners.push(listener); }.bind(this); /** * Adds a listener that will be called when content and all ads have * finished playing. * @param {function} listener The listener to be called when content and ads complete. */ this.addContentAndAdsEndedListener = function(listener) { this.contentAndAdsEndedListeners.push(listener); }.bind(this); /** * Sets the listener to be called to trigger manual ad break playback. * @param {function} listener The listener to be called to trigger manual ad break playback. */ this.setAdBreakReadyListener = function(listener) { this.adBreakReadyListener = listener; }.bind(this); /** * Pauses the ad. */ this.pauseAd = function() { if (this.adsActive && this.adPlaying) { showPauseButton(); this.adsManager.pause(); this.adPlaying = false; } }.bind(this); /** * Resumes the ad. */ this.resumeAd = function() { if (this.adsActive && !this.adPlaying) { showPlayButton(); this.adsManager.resume(); this.adPlaying = true; } }.bind(this); /** * Set up intervals to check for seeking and update current video time. * @private */ var setUpPlayerIntervals_ = function() { this.updateTimeIntervalHandle = setInterval(updateCurrentTime_, this.seekCheckInterval); this.seekCheckIntervalHandle = setInterval(checkForSeeking_, this.seekCheckInterval); this.resizeCheckIntervalHandle = setInterval(checkForResize_, this.resizeCheckInterval); }.bind(this); /** * Updates the start time of the video * @private */ var updateStartTime_ = function(){ var cur = this.player.currentTime(); if (!cur || this.player.ads.state!='content-playback') return; // first time that isn't zero is our start time, but only if it's // more than the 1sec if (cur<1) cur = 0; this.contentPlayheadTracker.startTime = cur; this.player.off('timeupdate', updateStartTime_); }.bind(this); /** * Updates the current time of the video * @private */ var updateCurrentTime_ = function() { if (this.player.ads.state=='content-playback' && !this.contentPlayheadTracker.seeking && this.contentPlayheadTracker.startTime>=0) { this.contentPlayheadTracker.currentTime = this.player.currentTime() - this.contentPlayheadTracker.startTime; } }.bind(this); /** * 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. * * @private */ var checkForSeeking_ = function() { if (this.player.ads.state!='content-playback') return; var tempCurrentTime = this.player.currentTime(); var 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.player.currentTime(); }.bind(this); /** * Detects when the player is resized (for fluid support) and resizes the * ads manager to match. * * @private */ var checkForResize_ = function() { var currentWidth = this.getPlayerWidth(); var currentHeight = this.getPlayerHeight(); if (this.adsManager && (currentWidth != this.adsManagerDimensions.width || currentHeight != this.adsManagerDimensions.height)) { this.adsManagerDimensions.width = currentWidth; this.adsManagerDimensions.height = currentHeight; this.adsManager.resize(currentWidth, currentHeight, google.ima.ViewMode.NORMAL); } }.bind(this); /** * Changes the flag to show or hide the ad countdown timer. * * @param {boolean} showCountdownIn Show or hide the countdown timer. */ this.setShowCountdown = function(showCountdownIn) { this.showCountdown = showCountdownIn; this.countdownDiv.style.display = this.showCountdown ? '' : 'none'; }.bind(this); /** * Current plugin version. */ this.VERSION = '0.2.0'; /** * Stores user-provided settings. */ this.settings; /** * Used to prefix videojs ima */ this.controlPrefix; /** * Video element playing content. */ this.contentPlayer; /** * Boolean flag to show or hide the ad countdown timer. */ this.showCountdown; /** * Boolena flag to enable manual ad break playback. */ this.autoPlayAdBreaks; /** * Video.js control bar. */ this.vjsControls; /** * Div used as an ad container. */ this.adContainerDiv; /** * Div used to display ad controls. */ this.controlsDiv; /** * Div used to display ad countdown timer. */ this.countdownDiv; /** * Div used to display add seek bar. */ this.seekBarDiv; /** * Div used to display ad progress (in seek bar). */ this.progressDiv; /** * Div used to display ad play/pause button. */ this.playPauseDiv; /** * Div used to display ad mute button. */ this.muteDiv; /** * Div used by the volume slider. */ this.sliderDiv; /** * Volume slider level visuals */ this.sliderLevelDiv; /** * Div used to display ad fullscreen button. */ this.fullscreenDiv; /** * IMA SDK AdDisplayContainer. */ this.adDisplayContainer; /** * True if the AdDisplayContainer has been initialized. False otherwise. */ this.adDisplayContainerInitialized = false; /** * IMA SDK AdsLoader */ this.adsLoader; /** * IMA SDK AdsManager */ this.adsManager; /** * IMA SDK AdsRenderingSettings. */ this.adsRenderingSettings = null; /** * Ad tag URL. Should return VAST, VMAP, or ad rules. */ this.adTagUrl; /** * VAST, VMAP, or ad rules response. Used in lieu of fetching a response * from an ad tag URL. */ this.adsResponse; /** * Current IMA SDK Ad. */ this.currentAd; /** * Timer used to track content progress. */ this.contentTrackingTimer; /** * Timer used to track ad progress. */ this.adTrackingTimer; /** * True if ads are currently displayed, false otherwise. * True regardless of ad pause state if an ad is currently being displayed. */ this.adsActive = false; /** * True if ad is currently playing, false if ad is paused or ads are not * currently displayed. */ this.adPlaying = false; /** * True if the ad is muted, false otherwise. */ this.adMuted = false; /** * True if our content video has completed, false otherwise. */ this.contentComplete = false; /** * True if ALL_ADS_COMPLETED has fired, false until then. */ this.allAdsCompleted = false; /** * Handle to interval that repeatedly updates current time. */ this.updateTimeIntervalHandle; /** * Handle to interval that repeatedly checks for seeking. */ this.seekCheckIntervalHandle; /** * 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; /** * 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; /** * Stores data for the content playhead tracker. */ this.contentPlayheadTracker = { currentTime: 0, previousTime: 0, seeking: false, duration: 0, startTime: -1 }; /** * Stores data for the ad playhead tracker. */ this.adPlayheadTracker = { currentTime: 0, duration: 0, isPod: false, adPosition: 0, totalAds: 0 }; /** * Stores the dimensions for the ads manager. */ this.adsManagerDimensions = { width: 0, height: 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. */ this.contentEndedListeners = []; /** * Content and ads ended listeners passed by the publisher to the plugin. * These will be called when the plugin detects that content *and all * ads* have completed. This differs from the contentEndedListeners in that * contentEndedListeners will fire between content ending and a post-roll * playing, whereas the contentAndAdsEndedListeners will fire after the * post-roll completes. */ this.contentAndAdsEndedListeners = []; /** * Listener to be called to trigger manual ad break playback. */ this.adBreakReadyListener = function() { console.log('Please set adBreakReadyListener'); }; /** * Stores the content source so we can re-populate it manually after a * post-roll on iOS. */ this.contentSource = ''; /** * Local content ended listener for contentComplete. */ this.localContentEndedListener = function() { if (this.adsLoader && !this.contentComplete) { this.adsLoader.contentComplete(); this.contentComplete = true; } for (var index in this.contentEndedListeners) { this.contentEndedListeners[index](); } if (this.allAdsCompleted) { for (var index in this.contentAndAdsEndedListeners) { this.contentAndAdsEndedListeners[index](); } } clearInterval(this.updateTimeIntervalHandle); clearInterval(this.seekCheckIntervalHandle); clearInterval(this.resizeCheckIntervalHandle); if(this.player.el()) { this.player.one('play', setUpPlayerIntervals_); } }.bind(this); this.playerDisposedListener = function(){ this.contentEndedListeners, this.contentAndAdsEndedListeners = [], []; this.contentComplete = true; this.player.off('contentended', this.localContentEndedListener); this.player.off('timeupdate', updateStartTime_); // Bug fix: https://github.com/googleads/videojs-ima/issues/306 if (this.player.ads.adTimeoutTimeout) { clearTimeout(this.player.ads.adTimeoutTimeout); } var intervalsToClear = [this.updateTimeIntervalHandle, this.seekCheckIntervalHandle, this.adTrackingTimer, this.resizeCheckIntervalHandle]; for (var index in intervalsToClear) { var interval = intervalsToClear[index]; if (interval) { clearInterval(interval); } } if (this.adsManager) { this.adsManager.destroy(); this.adsManager = null; } }.bind(this); this.initVjsControls = function() { var _this = this; var override = function(cls, obj, method, fn, always) { var orig = cls.prototype[method]; return obj[method] = function() { return _this.adsActive || always ? fn && fn.apply(this, arguments) : orig && orig.apply(this, arguments); }; }; var overrideHandler = function(cls, obj, target, event, method, fn, always) { var orig = cls.prototype[method]; var handler = override(cls, obj, method, fn); if (target) { obj.off(target, event, orig); obj.on(target, event, handler); } else { obj.off(event, orig); obj.on(event, handler); } }; var PlayToggle = videojs.getComponent('PlayToggle'); var playToggle = this.vjsControls.playToggle; overrideHandler(PlayToggle