UNPKG

videojs-ima

Version:

[![Build Status](https://travis-ci.org/googleads/videojs-ima.svg?branch=master)](https://travis-ci.org/googleads/videojs-ima)

799 lines (677 loc) 21.4 kB
/** * 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 */ import pkg from '../package.json'; /** * Implementation of the IMA SDK for the plugin. * * @param {Object} controller Reference to the parent controller. * * @constructor * @struct * @final */ const SdkImpl = function(controller) { /** * Plugin controller. */ this.controller = controller; /** * IMA SDK AdDisplayContainer. */ this.adDisplayContainer = null; /** * True if the AdDisplayContainer has been initialized. False otherwise. */ this.adDisplayContainerInitialized = false; /** * IMA SDK AdsLoader */ this.adsLoader = null; /** * IMA SDK AdsManager */ this.adsManager = null; /** * IMA SDK AdsRenderingSettings. */ this.adsRenderingSettings = null; /** * VAST, VMAP, or ad rules response. Used in lieu of fetching a response * from an ad tag URL. */ this.adsResponse = null; /** * Current IMA SDK Ad. */ this.currentAd = null; /** * Timer used to track ad progress. */ this.adTrackingTimer = null; /** * True if ALL_ADS_COMPLETED has fired, false until then. */ this.allAdsCompleted = false; /** * 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; /** * Listener to be called to trigger manual ad break playback. */ this.adBreakReadyListener = undefined; /** * Tracks whether or not we have already called adsLoader.contentComplete(). */ this.contentCompleteCalled = false; /** * Stores the dimensions for the ads manager. */ this.adsManagerDimensions = { width: 0, height: 0, }; /** * Boolean flag to enable manual ad break playback. */ this.autoPlayAdBreaks = true; if (this.controller.getSettings().autoPlayAdBreaks === false) { this.autoPlayAdBreaks = false; } // Set SDK settings from plugin settings. if (this.controller.getSettings().locale) { /* eslint no-undef: 'error' */ /* global google */ google.ima.settings.setLocale(this.controller.getSettings().locale); } if (this.controller.getSettings().disableFlashAds) { google.ima.settings.setDisableFlashAds( this.controller.getSettings().disableFlashAds); } if (this.controller.getSettings().disableCustomPlaybackForIOS10Plus) { google.ima.settings.setDisableCustomPlaybackForIOS10Plus( this.controller.getSettings().disableCustomPlaybackForIOS10Plus); } }; /** * Creates and initializes the IMA SDK objects. */ SdkImpl.prototype.initAdObjects = function() { this.adDisplayContainer = new google.ima.AdDisplayContainer( this.controller.getAdContainerDiv(), this.controller.getContentPlayer()); this.adsLoader = new google.ima.AdsLoader(this.adDisplayContainer); this.adsLoader.getSettings().setVpaidMode( google.ima.ImaSdkSettings.VpaidMode.ENABLED); if (this.controller.getSettings().vpaidAllowed == false) { this.adsLoader.getSettings().setVpaidMode( google.ima.ImaSdkSettings.VpaidMode.DISABLED); } if (this.controller.getSettings().vpaidMode) { this.adsLoader.getSettings().setVpaidMode( this.controller.getSettings().vpaidMode); } if (this.controller.getSettings().locale) { this.adsLoader.getSettings().setLocale( this.controller.getSettings().locale); } if (this.controller.getSettings().numRedirects) { this.adsLoader.getSettings().setNumRedirects( this.controller.getSettings().numRedirects); } this.adsLoader.getSettings().setPlayerType('videojs-ima'); this.adsLoader.getSettings().setPlayerVersion(pkg.version); this.adsLoader.getSettings().setAutoPlayAdBreaks(this.autoPlayAdBreaks); this.adsLoader.addEventListener( google.ima.AdsManagerLoadedEvent.Type.ADS_MANAGER_LOADED, this.onAdsManagerLoaded.bind(this), false); this.adsLoader.addEventListener( google.ima.AdErrorEvent.Type.AD_ERROR, this.onAdsLoaderError.bind(this), false); this.controller.playerWrapper.vjsPlayer.trigger({ type: 'ads-loader', adsLoader: this.adsLoader, }); }; /** * Creates the AdsRequest and request ads through the AdsLoader. */ SdkImpl.prototype.requestAds = function() { const adsRequest = new google.ima.AdsRequest(); if (this.controller.getSettings().adTagUrl) { adsRequest.adTagUrl = this.controller.getSettings().adTagUrl; } else { adsRequest.adsResponse = this.controller.getSettings().adsResponse; } if (this.controller.getSettings().forceNonLinearFullSlot) { adsRequest.forceNonLinearFullSlot = true; } if (this.controller.getSettings().vastLoadTimeout) { adsRequest.vastLoadTimeout = this.controller.getSettings().vastLoadTimeout; } adsRequest.linearAdSlotWidth = this.controller.getPlayerWidth(); adsRequest.linearAdSlotHeight = this.controller.getPlayerHeight(); adsRequest.nonLinearAdSlotWidth = this.controller.getSettings().nonLinearWidth || this.controller.getPlayerWidth(); adsRequest.nonLinearAdSlotHeight = this.controller.getSettings().nonLinearHeight || this.controller.getPlayerHeight(); adsRequest.setAdWillAutoPlay(this.controller.adsWillAutoplay()); adsRequest.setAdWillPlayMuted(this.controller.adsWillPlayMuted()); // Populate the adsRequestproperties with those provided in the AdsRequest // object in the settings. let providedAdsRequest = this.controller.getSettings().adsRequest; if (providedAdsRequest && typeof providedAdsRequest === 'object') { Object.keys(providedAdsRequest).forEach((key) => { adsRequest[key] = providedAdsRequest[key]; }); } this.adsLoader.requestAds(adsRequest); this.controller.playerWrapper.vjsPlayer.trigger({ type: 'ads-request', AdsRequest: adsRequest, }); }; /** * Listener for the ADS_MANAGER_LOADED event. Creates the AdsManager, * sets up event listeners, and triggers the 'adsready' event for * videojs-ads-contrib. * * @param {google.ima.AdsManagerLoadedEvent} adsManagerLoadedEvent Fired when * the AdsManager loads. */ SdkImpl.prototype.onAdsManagerLoaded = function(adsManagerLoadedEvent) { this.createAdsRenderingSettings(); this.adsManager = adsManagerLoadedEvent.getAdsManager( this.controller.getContentPlayheadTracker(), this.adsRenderingSettings); this.adsManager.addEventListener( google.ima.AdErrorEvent.Type.AD_ERROR, this.onAdError.bind(this)); this.adsManager.addEventListener( google.ima.AdEvent.Type.AD_BREAK_READY, this.onAdBreakReady.bind(this)); this.adsManager.addEventListener( google.ima.AdEvent.Type.CONTENT_PAUSE_REQUESTED, this.onContentPauseRequested.bind(this)); this.adsManager.addEventListener( google.ima.AdEvent.Type.CONTENT_RESUME_REQUESTED, this.onContentResumeRequested.bind(this)); this.adsManager.addEventListener( google.ima.AdEvent.Type.ALL_ADS_COMPLETED, this.onAllAdsCompleted.bind(this)); this.adsManager.addEventListener( google.ima.AdEvent.Type.LOADED, this.onAdLoaded.bind(this)); this.adsManager.addEventListener( google.ima.AdEvent.Type.STARTED, this.onAdStarted.bind(this)); this.adsManager.addEventListener( google.ima.AdEvent.Type.COMPLETE, this.onAdComplete.bind(this)); this.adsManager.addEventListener( google.ima.AdEvent.Type.SKIPPED, this.onAdComplete.bind(this)); this.adsManager.addEventListener( google.ima.AdEvent.Type.LOG, this.onAdLog.bind(this)); if (this.controller.getIsMobile()) { // Show/hide controls on pause and resume (triggered by tap). this.adsManager.addEventListener( google.ima.AdEvent.Type.PAUSED, this.onAdPaused.bind(this)); this.adsManager.addEventListener( google.ima.AdEvent.Type.RESUMED, this.onAdResumed.bind(this)); } this.controller.playerWrapper.vjsPlayer.trigger({ type: 'ads-manager', adsManager: this.adsManager, }); if (!this.autoPlayAdBreaks) { this.initAdsManager(); } this.controller.onAdsReady(); if (this.controller.getSettings().adsManagerLoadedCallback) { this.controller.getSettings().adsManagerLoadedCallback(); } }; /** * 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 */ SdkImpl.prototype.onAdsLoaderError = function(event) { window.console.warn('AdsLoader error: ' + event.getError()); this.controller.onErrorLoadingAds(event); if (this.adsManager) { this.adsManager.destroy(); } }; /** * Initialize the ads manager. */ SdkImpl.prototype.initAdsManager = function() { try { const initWidth = this.controller.getPlayerWidth(); const initHeight = this.controller.getPlayerHeight(); this.adsManagerDimensions.width = initWidth; this.adsManagerDimensions.height = initHeight; this.adsManager.init( initWidth, initHeight, google.ima.ViewMode.NORMAL); this.adsManager.setVolume(this.controller.getPlayerVolume()); this.initializeAdDisplayContainer(); } catch (adError) { this.onAdError(adError); } }; /** * Create AdsRenderingSettings for the IMA SDK. */ SdkImpl.prototype.createAdsRenderingSettings = function() { this.adsRenderingSettings = new google.ima.AdsRenderingSettings(); this.adsRenderingSettings.restoreCustomPlaybackStateOnAdBreakComplete = true; if (this.controller.getSettings().adsRenderingSettings) { for (let setting in this.controller.getSettings().adsRenderingSettings) { if (setting !== '') { this.adsRenderingSettings[setting] = this.controller.getSettings().adsRenderingSettings[setting]; } } } }; /** * Listener for errors thrown by the AdsManager. * @param {google.ima.AdErrorEvent} adErrorEvent The error event thrown by * the AdsManager. */ SdkImpl.prototype.onAdError = function(adErrorEvent) { const errorMessage = adErrorEvent.getError !== undefined ? adErrorEvent.getError() : adErrorEvent.stack; window.console.warn('Ad error: ' + errorMessage); this.adsManager.destroy(); this.controller.onAdError(adErrorEvent); // reset these so consumers don't think we are still in an ad break, // but reset them after any prior cleanup happens this.adsActive = false; this.adPlaying = false; }; /** * Listener for AD_BREAK_READY. Passes event on to publisher's listener. * @param {google.ima.AdEvent} adEvent AdEvent thrown by the AdsManager. */ SdkImpl.prototype.onAdBreakReady = function(adEvent) { this.adBreakReadyListener(adEvent); }; /** * Pauses the content video and displays the ad container so ads can play. * @param {google.ima.AdEvent} adEvent The AdEvent thrown by the AdsManager. */ SdkImpl.prototype.onContentPauseRequested = function(adEvent) { this.adsActive = true; this.adPlaying = true; this.controller.onAdBreakStart(adEvent); }; /** * Resumes content video and hides the ad container. * @param {google.ima.AdEvent} adEvent The AdEvent thrown by the AdsManager. */ SdkImpl.prototype.onContentResumeRequested = function(adEvent) { this.adsActive = false; this.adPlaying = false; this.controller.onAdBreakEnd(); // Hide controls in case of future non-linear ads. They'll be unhidden in // content_pause_requested. }; /** * Records that ads have completed and calls contentAndAdsEndedListeners * if content is also complete. * @param {google.ima.AdEvent} adEvent The AdEvent thrown by the AdsManager. */ SdkImpl.prototype.onAllAdsCompleted = function(adEvent) { this.allAdsCompleted = true; this.controller.onAllAdsCompleted(); }; /** * Starts the content video when a non-linear ad is loaded. * @param {google.ima.AdEvent} adEvent The AdEvent thrown by the AdsManager. */ SdkImpl.prototype.onAdLoaded = function(adEvent) { if (!adEvent.getAd().isLinear()) { this.controller.onNonLinearAdLoad(); this.controller.playContent(); } }; /** * 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. */ SdkImpl.prototype.onAdStarted = function(adEvent) { this.currentAd = adEvent.getAd(); if (this.currentAd.isLinear()) { this.adTrackingTimer = setInterval( this.onAdPlayheadTrackerInterval.bind(this), 250); this.controller.onLinearAdStart(); } else { this.controller.onNonLinearAdStart(); } }; /** * Handles an ad click. Puts the player UI in a paused state. */ SdkImpl.prototype.onAdPaused = function() { this.controller.onAdsPaused(); }; /** * Syncs controls when an ad resumes. * @param {google.ima.AdEvent} adEvent The AdEvent thrown by the AdsManager. */ SdkImpl.prototype.onAdResumed = function(adEvent) { this.controller.onAdsResumed(); }; /** * Clears the interval timer for current ad time when an ad completes. */ SdkImpl.prototype.onAdComplete = function() { if (this.currentAd.isLinear()) { clearInterval(this.adTrackingTimer); } }; /** * Handles ad log messages. * @param {google.ima.AdEvent} adEvent The AdEvent thrown by the AdsManager. */ SdkImpl.prototype.onAdLog = function(adEvent) { this.controller.onAdLog(adEvent); }; /** * Gets the current time and duration of the ad and calls the method to * update the ad UI. */ SdkImpl.prototype.onAdPlayheadTrackerInterval = function() { if (this.adsManager === null) return; const remainingTime = this.adsManager.getRemainingTime(); const duration = this.currentAd.getDuration(); let currentTime = duration - remainingTime; currentTime = currentTime > 0 ? currentTime : 0; let totalAds = 0; let adPosition; if (this.currentAd.getAdPodInfo()) { adPosition = this.currentAd.getAdPodInfo().getAdPosition(); totalAds = this.currentAd.getAdPodInfo().getTotalAds(); } this.controller.onAdPlayheadUpdated( currentTime, remainingTime, duration, adPosition, totalAds); }; /** * Called by the player wrapper when content completes. */ SdkImpl.prototype.onContentComplete = function() { if (this.adsLoader) { this.adsLoader.contentComplete(); this.contentCompleteCalled = true; } if (this.adsManager && this.adsManager.getCuePoints() && !this.adsManager.getCuePoints().includes(-1)) { this.controller.onNoPostroll(); } if (this.allAdsCompleted) { this.controller.onContentAndAdsCompleted(); } }; /** * Called when the player is disposed. */ SdkImpl.prototype.onPlayerDisposed = function() { if (this.adTrackingTimer) { clearInterval(this.adTrackingTimer); } if (this.adsManager) { this.adsManager.destroy(); this.adsManager = null; } }; SdkImpl.prototype.onPlayerReadyForPreroll = function() { if (this.autoPlayAdBreaks) { this.initAdsManager(); try { this.controller.showAdContainer(); // Sync ad volume with content volume. this.adsManager.setVolume(this.controller.getPlayerVolume()); this.adsManager.start(); } catch (adError) { this.onAdError(adError); } } }; SdkImpl.prototype.onPlayerReady = function() { this.initAdObjects(); if ((this.controller.getSettings().adTagUrl || this.controller.getSettings().adsResponse) && this.controller.getSettings().requestMode === 'onLoad') { this.requestAds(); } }; SdkImpl.prototype.onPlayerEnterFullscreen = function() { if (this.adsManager) { this.adsManager.resize( window.screen.width, window.screen.height, google.ima.ViewMode.FULLSCREEN); } }; SdkImpl.prototype.onPlayerExitFullscreen = function() { if (this.adsManager) { this.adsManager.resize( this.controller.getPlayerWidth(), this.controller.getPlayerHeight(), google.ima.ViewMode.NORMAL); } }; /** * Called when the player volume changes. * * @param {number} volume The new player volume. */ SdkImpl.prototype.onPlayerVolumeChanged = function(volume) { if (this.adsManager) { this.adsManager.setVolume(volume); } if (volume == 0) { this.adMuted = true; } else { this.adMuted = false; } }; /** * Called when the player wrapper detects that the player has been resized. * * @param {number} width The post-resize width of the player. * @param {number} height The post-resize height of the player. */ SdkImpl.prototype.onPlayerResize = function(width, height) { if (this.adsManager) { this.adsManagerDimensions.width = width; this.adsManagerDimensions.height = height; /* global google */ /* eslint no-undef: 'error' */ this.adsManager.resize(width, height, google.ima.ViewMode.NORMAL); } }; /** * @return {Object} The current ad. */ SdkImpl.prototype.getCurrentAd = function() { return this.currentAd; }; /** * Listener JSDoc for ESLint. This listener can be passed to * setAdBreakReadyListener. * @callback listener */ /** * Sets the listener to be called to trigger manual ad break playback. * @param {listener} listener The listener to be called to trigger manual ad * break playback. */ SdkImpl.prototype.setAdBreakReadyListener = function(listener) { this.adBreakReadyListener = listener; }; /** * @return {boolean} True if an ad is currently playing. False otherwise. */ SdkImpl.prototype.isAdPlaying = function() { return this.adPlaying; }; /** * @return {boolean} True if an ad is currently playing. False otherwise. */ SdkImpl.prototype.isAdMuted = function() { return this.adMuted; }; /** * Pause ads. */ SdkImpl.prototype.pauseAds = function() { this.adsManager.pause(); this.adPlaying = false; }; /** * Resume ads. */ SdkImpl.prototype.resumeAds = function() { this.adsManager.resume(); this.adPlaying = true; }; /** * Unmute ads. */ SdkImpl.prototype.unmute = function() { this.adsManager.setVolume(1); this.adMuted = false; }; /** * Mute ads. */ SdkImpl.prototype.mute = function() { this.adsManager.setVolume(0); this.adMuted = true; }; /** * Set the volume of the ads. 0-1. * * @param {number} volume The new volume. */ SdkImpl.prototype.setVolume = function(volume) { this.adsManager.setVolume(volume); if (volume == 0) { this.adMuted = true; } else { this.adMuted = false; } }; /** * Initializes the AdDisplayContainer. On mobile, this must be done as a * result of user action. */ SdkImpl.prototype.initializeAdDisplayContainer = function() { if (this.adDisplayContainer) { if (!this.adDisplayContainerInitialized) { this.adDisplayContainer.initialize(); this.adDisplayContainerInitialized = true; } } }; /** * Called by publishers in manual ad break playback mode to start an ad * break. */ SdkImpl.prototype.playAdBreak = function() { if (!this.autoPlayAdBreaks) { this.controller.showAdContainer(); // Sync ad volume with content volume. this.adsManager.setVolume(this.controller.getPlayerVolume()); this.adsManager.start(); } }; /** * Callback JSDoc for ESLint. This callback can be passed to addEventListener. * @callback callback */ /** * 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 {callback} callback The method to call when the event is fired. */ SdkImpl.prototype.addEventListener = function(event, callback) { if (this.adsManager) { this.adsManager.addEventListener(event, callback); } }; /** * Returns the instance of the AdsManager. * @return {google.ima.AdsManager} The AdsManager being used by the plugin. */ SdkImpl.prototype.getAdsManager = function() { return this.adsManager; }; /** * Reset the SDK implementation. */ SdkImpl.prototype.reset = function() { this.adsActive = false; this.adPlaying = false; 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.contentCompleteCalled) { this.adsLoader.contentComplete(); } this.contentCompleteCalled = false; this.allAdsCompleted = false; }; export default SdkImpl;