aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/js/plugins/ads.js281
1 files changed, 213 insertions, 68 deletions
diff --git a/src/js/plugins/ads.js b/src/js/plugins/ads.js
index 06b4eee5..956fbb6e 100644
--- a/src/js/plugins/ads.js
+++ b/src/js/plugins/ads.js
@@ -1,6 +1,6 @@
import utils from '../utils';
-// Events are different on various devices. We det the correct events, based on userAgent.
+// Events are different on various devices. We set the correct events, based on userAgent.
const getStartEvents = () => {
let events = ['click'];
@@ -38,16 +38,43 @@ export default class Ads {
}
ready() {
+ this.time = Date.now();
this.startEvents = getStartEvents();
this.adDisplayContainer = null;
this.adDisplayElement = null;
this.adsManager = null;
this.adsLoader = null;
- this.adCuePoints = null;
+ this.adsCuePoints = null;
this.currentAd = null;
this.events = {};
+ this.safetyTimer = null;
this.videoElement = document.createElement('video');
+ // Setup a simple promise to resolve if the IMA loader is ready.
+ this.adsLoaderResolve = () => {};
+ this.adsLoaderPromise = new Promise((resolve) => {
+ this.adsLoaderResolve = resolve;
+ });
+ this.adsLoaderPromise.then(() => {
+ this.player.debug.log(`[${(Date.now() - this.time) / 1000}s][IMA SDK] adsLoader resolved!`, this.adsLoader);
+ });
+
+ // Setup a promise to resolve if the IMA manager is ready.
+ this.adsManagerResolve = () => {};
+ this.adsManagerPromise = new Promise((resolve) => {
+ // Resolve our promise.
+ this.adsManagerResolve = resolve;
+ });
+ this.adsManagerPromise.then(() => {
+ // Clear the safety timer.
+ this.clearSafetyTimer('onAdsManagerLoaded()');
+ this.player.debug.log(`[${(Date.now() - this.time) / 1000}s][IMA SDK] adsManager resolved!`, this.adsManager);
+ });
+
+ // Start ticking our safety timer. If the whole advertisement
+ // thing doesn't resolve within our set time; we bail.
+ this.startSafetyTimer(12000, 'ready()');
+
// Setup the ad display container.
this.setupAdDisplayContainer();
@@ -83,6 +110,8 @@ export default class Ads {
adsRequest.nonLinearAdSlotHeight = container.offsetHeight;
this.adsLoader.requestAds(adsRequest);
+
+ this.adsLoaderResolve();
}
onAdsManagerLoaded(adsManagerLoadedEvent) {
@@ -111,6 +140,9 @@ export default class Ads {
this.adsManager.addEventListener(window.google.ima.AdEvent.Type.LOADED, event => this.onAdEvent(event));
this.adsManager.addEventListener(window.google.ima.AdEvent.Type.STARTED, event => this.onAdEvent(event));
this.adsManager.addEventListener(window.google.ima.AdEvent.Type.COMPLETE, event => this.onAdEvent(event));
+
+ // Resolve our adsManager.
+ this.adsManagerResolve();
}
onAdEvent(event) {
@@ -127,9 +159,58 @@ export default class Ads {
// let intervalTimer;
switch (event.type) {
- case window.google.ima.AdEvent.Type.LOADED:
+
+ case google.ima.AdEvent.Type.AD_BREAK_READY:
+ // This event indicates that a mid-roll ad is ready to start.
+ // We pause the player and tell the adsManager to start playing the ad.
+ this.player.debug.log(`[${(Date.now() - this.time) / 1000}s][IMA SDK] AD_BREAK_READY |`, 'Fired when an ad rule or a VMAP ad break would have played if autoPlayAdBreaks is false.');
+
+ this.player.pause();
+
+ this.adsManager.start();
+
+ this.handleEventListeners('AD_BREAK_READY');
+ break;
+ case google.ima.AdEvent.Type.AD_METADATA:
+ this.player.debug.log(`[${(Date.now() - this.time) / 1000}s][IMA SDK] AD_METADATA |`, 'Fired when an ads list is loaded.');
+ break;
+ case google.ima.AdEvent.Type.ALL_ADS_COMPLETED:
+ this.player.debug.log(`[${(Date.now() - this.time) / 1000}s][IMA SDK] ALL_ADS_COMPLETED |`, 'Fired when the ads manager is done playing all the ads.');
+ this.handleEventListeners('ALL_ADS_COMPLETED');
+ break;
+ case google.ima.AdEvent.Type.CLICK:
+ this.player.debug.log(`[${(Date.now() - this.time) / 1000}s][IMA SDK] CLICK |`, 'Fired when the ad is clicked.');
+ break;
+ case google.ima.AdEvent.Type.COMPLETE:
+ // This event indicates the ad has finished - the video player
+ // can perform appropriate UI actions, such as removing the timer for
+ // remaining time detection.
+ // clearInterval(intervalTimer);
+ this.player.debug.log(`[${(Date.now() - this.time) / 1000}s][IMA SDK] COMPLETE |`, 'Fired when the ad completes playing.');
+
+ this.handleEventListeners('COMPLETE');
+
+ this.adDisplayElement.style.display = 'none';
+
+ if (this.player.currentTime < this.player.duration) {
+ this.player.play();
+ }
+ break;
+ case window.google.ima.AdEvent.Type.CONTENT_PAUSE_REQUESTED:
+ this.player.debug.log(`[${(Date.now() - this.time) / 1000}s][IMA SDK] CONTENT_PAUSE_REQUESTED |`, 'Fired when content should be paused. This usually happens right before an ad is about to cover the content.');
+
+ this.handleEventListeners('CONTENT_PAUSE_REQUESTED');
+ break;
+
+ case window.google.ima.AdEvent.Type.CONTENT_RESUME_REQUESTED:
+ this.player.debug.log(`[${(Date.now() - this.time) / 1000}s][IMA SDK] CONTENT_RESUME_REQUESTED |`, 'Fired when content should be resumed. This usually happens when an ad finishes or collapses.');
+
+ this.handleEventListeners('CONTENT_RESUME_REQUESTED');
+ break;
+ case google.ima.AdEvent.Type.LOADED:
// This is the first event sent for an ad - it is possible to
// determine whether the ad is a video ad or an overlay.
+ this.player.debug.log(`[${(Date.now() - this.time) / 1000}s][IMA SDK] LOADED |`, event.getAd().getContentType());
// Show the ad display element.
this.adDisplayElement.style.display = 'block';
@@ -141,59 +222,60 @@ export default class Ads {
ad.width = container.offsetWidth;
ad.height = container.offsetHeight;
}
- break;
- case window.google.ima.AdEvent.Type.STARTED:
+ // console.info('Ad type: ' + event.getAd().getAdPodInfo().getPodIndex());
+ // console.info('Ad time: ' + event.getAd().getAdPodInfo().getTimeOffset());
+ break;
+ case google.ima.AdEvent.Type.STARTED:
// This event indicates the ad has started - the video player
// can adjust the UI, for example display a pause button and
// remaining time.
+ this.player.debug.log(`[${(Date.now() - this.time) / 1000}s][IMA SDK] STARTED |`, 'Fired when the ad starts playing.');
this.player.pause();
this.handleEventListeners('STARTED');
-
- // if (ad.isLinear()) {
- // For a linear ad, a timer can be started to poll for
- // the remaining time.
- // intervalTimer = setInterval(
- // () => {
- // let remainingTime = this.adsManager.getRemainingTime();
- // console.log(remainingTime);
- // },
- // 300); // every 300ms
- // }
break;
-
- case window.google.ima.AdEvent.Type.CONTENT_PAUSE_REQUESTED:
- this.handleEventListeners('CONTENT_PAUSE_REQUESTED');
+ case google.ima.AdEvent.Type.DURATION_CHANGE:
+ this.player.debug.log(`[${(Date.now() - this.time) / 1000}s][IMA SDK] DURATION_CHANGE |`, 'Fired when the ad\'s duration changes.');
break;
-
- case window.google.ima.AdEvent.Type.CONTENT_RESUME_REQUESTED:
- this.handleEventListeners('CONTENT_RESUME_REQUESTED');
+ case google.ima.AdEvent.Type.FIRST_QUARTILE:
+ this.player.debug.log(`[${(Date.now() - this.time) / 1000}s][IMA SDK] FIRST_QUARTILE |`, 'Fired when the ad playhead crosses first quartile.');
break;
-
- case window.google.ima.AdEvent.Type.AD_BREAK_READY:
- // This event indicates that a mid-roll ad is ready to start.
- // We pause the player and tell the adsManager to start playing the ad.
- this.player.pause();
- this.adsManager.start();
- this.handleEventListeners('AD_BREAK_READY');
+ case google.ima.AdEvent.Type.IMPRESSION:
+ this.player.debug.log(`[${(Date.now() - this.time) / 1000}s][IMA SDK] IMPRESSION |`, 'Fired when the impression URL has been pinged.');
break;
-
- case window.google.ima.AdEvent.Type.COMPLETE:
- // This event indicates the ad has finished - the video player
- // can perform appropriate UI actions, such as removing the timer for
- // remaining time detection.
- // clearInterval(intervalTimer);
- this.handleEventListeners('COMPLETE');
-
- this.adDisplayElement.style.display = 'none';
- if (this.player.currentTime < this.player.duration) {
- this.player.play();
- }
+ case google.ima.AdEvent.Type.INTERACTION:
+ this.player.debug.log(`[${(Date.now() - this.time) / 1000}s][IMA SDK] INTERACTION |`, 'Fired when an ad triggers the interaction callback. Ad interactions contain an interaction ID string in the ad data.');
break;
-
- case window.google.ima.AdEvent.Type.ALL_ADS_COMPLETED:
- this.handleEventListeners('ALL_ADS_COMPLETED');
+ case google.ima.AdEvent.Type.LINEAR_CHANGED:
+ this.player.debug.log(`[${(Date.now() - this.time) / 1000}s][IMA SDK] LINEAR_CHANGED |`, 'Fired when the displayed ad changes from linear to nonlinear, or vice versa.');
+ break;
+ case google.ima.AdEvent.Type.MIDPOINT:
+ this.player.debug.log(`[${(Date.now() - this.time) / 1000}s][IMA SDK] MIDPOINT |`, 'Fired when the ad playhead crosses midpoint.');
+ break;
+ case google.ima.AdEvent.Type.PAUSED:
+ this.player.debug.log(`[${(Date.now() - this.time) / 1000}s][IMA SDK] PAUSED |`, 'Fired when the ad is paused.');
+ break;
+ case google.ima.AdEvent.Type.RESUMED:
+ this.player.debug.log(`[${(Date.now() - this.time) / 1000}s][IMA SDK] RESUMED |`, 'Fired when the ad is resumed.');
+ break;
+ case google.ima.AdEvent.Type.SKIPPABLE_STATE_CHANGED:
+ this.player.debug.log(`[${(Date.now() - this.time) / 1000}s][IMA SDK] SKIPPABLE_STATE_CHANGED |`, 'Fired when the displayed ads skippable state is changed.');
+ break;
+ case google.ima.AdEvent.Type.SKIPPED:
+ this.player.debug.log(`[${(Date.now() - this.time) / 1000}s][IMA SDK] SKIPPED |`, 'Fired when the ad is skipped by the user.');
+ break;
+ case google.ima.AdEvent.Type.THIRD_QUARTILE:
+ this.player.debug.log(`[${(Date.now() - this.time) / 1000}s][IMA SDK] THIRD_QUARTILE |`, 'Fired when the ad playhead crosses third quartile.');
+ break;
+ case google.ima.AdEvent.Type.USER_CLOSE:
+ this.player.debug.log(`[${(Date.now() - this.time) / 1000}s][IMA SDK] USER_CLOSE |`, 'Fired when the ad is closed by the user.');
+ break;
+ case google.ima.AdEvent.Type.VOLUME_CHANGED:
+ this.player.debug.log(`[${(Date.now() - this.time) / 1000}s][IMA SDK] VOLUME_CHANGED |`, 'Fired when the ad volume has changed.');
+ break;
+ case google.ima.AdEvent.Type.VOLUME_MUTED:
+ this.player.debug.log(`[${(Date.now() - this.time) / 1000}s][IMA SDK] VOLUME_MUTED |`, 'Fired when the ad volume has been muted.');
break;
default:
@@ -202,21 +284,44 @@ export default class Ads {
}
onAdError(adErrorEvent) {
- // Handle the error logging.
- this.adDisplayElement.remove();
-
- if (this.adsManager) {
- this.adsManager.destroy();
- }
+ this.cancel();
if (this.player.debug) {
throw new Error(adErrorEvent);
}
}
+ /**
+ * Destroy the adsManager so we can grab new ads after this.
+ * If we don't then we're not allowed to call new ads based
+ * on google policies, as they interpret this as an accidental
+ * video requests. https://developers.google.com/interactive-
+ * media-ads/docs/sdks/android/faq#8
+ */
+ cancel() {
+ this.player.debug.warn(`[${(Date.now() - this.time) / 1000}s][IMA SDK]`, 'Advertisement cancelled.');
+
+ // Todo: Removing the ad container might be problematic if we were to recreate the adsManager. Think of playlists. Every new video you need to request a new VAST xml and preload the advertisement.
+ this.adDisplayElement.remove();
+
+ // Tell our adsManager to go bye bye.
+ this.adsManagerPromise.then(() => {
+ if (this.adsManager) {
+ this.adsManager.destroy();
+ }
+ });
+ }
+
setupAdDisplayContainer() {
const { container } = this.player.elements;
+ // So we can run VPAID2.
+ google.ima.settings.setVpaidMode(google.ima.ImaSdkSettings.VpaidMode.ENABLED);
+
+ // Set language.
+ // Todo: Could make a config option out of this locale value.
+ google.ima.settings.setLocale('en');
+
// We assume the adContainer is the video container of the plyr element
// that will house the ads.
this.adDisplayContainer = new window.google.ima.AdDisplayContainer(container);
@@ -230,32 +335,41 @@ export default class Ads {
// Set class name on the adDisplayContainer element.
this.adDisplayElement.setAttribute('class', this.player.config.classNames.ads);
- // Play ads when clicked.
- this.setOnClickHandler(this.adDisplayElement, this.playAds);
+ // Play ads when clicked. Wait until the adsManager and adsLoader
+ // are both resolved.
+ Promise.all([
+ this.adsManagerPromise,
+ this.adsLoaderPromise,
+ ]).then(() => {
+ this.setOnClickHandler(this.adDisplayElement, this.playAds);
+ });
}
playAds() {
const { container } = this.player.elements;
- // Initialize the container. Must be done via a user action on mobile devices.
- this.adDisplayContainer.initialize();
+ // Play the requested advertisement whenever the adsManager is ready.
+ this.adsManagerPromise.then(() => {
+ // Initialize the container. Must be done via a user action on mobile devices.
+ this.adDisplayContainer.initialize();
- try {
- // Initialize the ads manager. Ad rules playlist will start at this time.
- this.adsManager.init(container.offsetWidth, container.offsetHeight, window.google.ima.ViewMode.NORMAL);
+ try {
+ // Initialize the ads manager. Ad rules playlist will start at this time.
+ this.adsManager.init(container.offsetWidth, container.offsetHeight, window.google.ima.ViewMode.NORMAL);
- // Call play to start showing the ad. Single video and overlay ads will
- // start at this time; the call will be ignored for ad rules.
- this.adsManager.start();
- } catch (adError) {
- // An error may be thrown if there was a problem with the VAST response.
- this.player.play();
- this.adDisplayElement.remove();
+ // Call play to start showing the ad. Single video and overlay ads will
+ // start at this time; the call will be ignored for ad rules.
+ this.adsManager.start();
+ } catch (adError) {
+ // An error may be thrown if there was a problem with the VAST response.
+ this.player.play();
+ this.adDisplayElement.remove();
- if (this.player.debug) {
- throw new Error(adError);
+ if (this.player.debug) {
+ throw new Error(adError);
+ }
}
- }
+ });
}
/**
@@ -314,7 +428,6 @@ export default class Ads {
* @param {element} element - The element on which to set the listener
* @param {function} callback - The callback which will be invoked once triggered.
*/
-
setOnClickHandler(element, callback) {
for (let i = 0; i < this.startEvents.length; i += 1) {
const startEvent = this.startEvents[i];
@@ -339,4 +452,36 @@ export default class Ads {
this.events[event] = callback;
return this;
}
+
+ /**
+ * startSafetyTimer
+ * Setup a safety timer for when the ad network
+ * doesn't respond for whatever reason. The advertisement has 12 seconds
+ * to get its shit together. We stop this timer when the advertisement
+ * is playing, or when a user action is required to start, then we
+ * clear the timer on ad ready.
+ * @param {Number} time
+ * @param {String} from
+ * @private
+ */
+ startSafetyTimer(time, from) {
+ this.player.debug.log(`[${(Date.now() - this.time) / 1000}s][IMA SDK]`, `Safety timer invoked timer from: ${from}`);
+ this.safetyTimer = window.setTimeout(() => {
+ this.cancel();
+ this.clearSafetyTimer('startSafetyTimer()');
+ }, time);
+ }
+
+ /**
+ * clearSafetyTimer
+ * @param {String} from
+ * @private
+ */
+ clearSafetyTimer(from) {
+ if (typeof this.safetyTimer !== 'undefined' && this.safetyTimer !== null) {
+ this.player.debug.log(`[${(Date.now() - this.time) / 1000}s][IMA SDK]`, `Safety timer cleared timer from: ${from}`);
+ clearTimeout(this.safetyTimer);
+ this.safetyTimer = undefined;
+ }
+ }
}