aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorFerdi <ferdiemmen@users.noreply.github.com>2018-01-15 20:13:51 +0100
committerGitHub <noreply@github.com>2018-01-15 20:13:51 +0100
commitc24c05226d059eab4fa78781502f305fd5040a0d (patch)
tree8914f2c6eb44f90332cf5af76ede08721a045f5a
parent8064405dbc44e24a5547aa79dbd7968926ec62a1 (diff)
parent4b0005c28eaab460be1b4f5f22fa5d1f3ffae522 (diff)
downloadplyr-c24c05226d059eab4fa78781502f305fd5040a0d.tar.lz
plyr-c24c05226d059eab4fa78781502f305fd5040a0d.tar.xz
plyr-c24c05226d059eab4fa78781502f305fd5040a0d.zip
Merge pull request #761 from gehaktmolen/beta-with-ads-promises
Beta with ads - Added promises, missing events, new ad tag and additional logging.
-rw-r--r--.gitignore2
-rw-r--r--demo/src/js/demo.js8
-rw-r--r--src/js/plugins/ads.js281
3 files changed, 221 insertions, 70 deletions
diff --git a/.gitignore b/.gitignore
index b01afe98..b17cd7c7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,3 +7,5 @@ index-*.html
npm-debug.log
*.webm
/package-lock.json
+.idea/
+
diff --git a/demo/src/js/demo.js b/demo/src/js/demo.js
index d20cecdb..b8650896 100644
--- a/demo/src/js/demo.js
+++ b/demo/src/js/demo.js
@@ -52,8 +52,12 @@ document.addEventListener('DOMContentLoaded', () => {
google: 'AIzaSyDrNwtN3nLH_8rjCmu5Wq3ZCm4MNAVdc0c',
},
ads: {
- tagUrl:
- 'http://go.aniview.com/api/adserver6/vast/?AV_PUBLISHERID=58c25bb0073ef448b1087ad6&AV_CHANNELID=5a0458dc28a06145e4519d21&AV_URL=127.0.0.1:3000&cb=1&AV_WIDTH=640&AV_HEIGHT=480',
+ tagUrl: 'https://pubads.g.doubleclick.net/gampad/ads' +
+ '?sz=640x480&iu=/124319096/external/ad_rule_samples&' +
+ 'ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&' +
+ 'output=vmap&unviewed_position_start=1&cust_params=d' +
+ 'eployment%3Ddevsite%26sample_ar%3Dpremidpostoptimiz' +
+ 'edpod&cmsid=496&vid=short_onecue&correlator=',
},
});
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;
+ }
+ }
}