Added promises, missing events, new ad tag and additional logging.

This commit is contained in:
Arthur Hulsman 2018-01-15 14:47:34 +01:00
parent 8064405dbc
commit 4b0005c28e
3 changed files with 221 additions and 70 deletions

2
.gitignore vendored
View File

@ -7,3 +7,5 @@ index-*.html
npm-debug.log npm-debug.log
*.webm *.webm
/package-lock.json /package-lock.json
.idea/

View File

@ -52,8 +52,12 @@ document.addEventListener('DOMContentLoaded', () => {
google: 'AIzaSyDrNwtN3nLH_8rjCmu5Wq3ZCm4MNAVdc0c', google: 'AIzaSyDrNwtN3nLH_8rjCmu5Wq3ZCm4MNAVdc0c',
}, },
ads: { ads: {
tagUrl: tagUrl: 'https://pubads.g.doubleclick.net/gampad/ads' +
'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', '?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=',
}, },
}); });

View File

@ -1,6 +1,6 @@
import utils from '../utils'; 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 = () => { const getStartEvents = () => {
let events = ['click']; let events = ['click'];
@ -38,16 +38,43 @@ export default class Ads {
} }
ready() { ready() {
this.time = Date.now();
this.startEvents = getStartEvents(); this.startEvents = getStartEvents();
this.adDisplayContainer = null; this.adDisplayContainer = null;
this.adDisplayElement = null; this.adDisplayElement = null;
this.adsManager = null; this.adsManager = null;
this.adsLoader = null; this.adsLoader = null;
this.adCuePoints = null; this.adsCuePoints = null;
this.currentAd = null; this.currentAd = null;
this.events = {}; this.events = {};
this.safetyTimer = null;
this.videoElement = document.createElement('video'); 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. // Setup the ad display container.
this.setupAdDisplayContainer(); this.setupAdDisplayContainer();
@ -83,6 +110,8 @@ export default class Ads {
adsRequest.nonLinearAdSlotHeight = container.offsetHeight; adsRequest.nonLinearAdSlotHeight = container.offsetHeight;
this.adsLoader.requestAds(adsRequest); this.adsLoader.requestAds(adsRequest);
this.adsLoaderResolve();
} }
onAdsManagerLoaded(adsManagerLoadedEvent) { 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.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.STARTED, event => this.onAdEvent(event));
this.adsManager.addEventListener(window.google.ima.AdEvent.Type.COMPLETE, event => this.onAdEvent(event)); this.adsManager.addEventListener(window.google.ima.AdEvent.Type.COMPLETE, event => this.onAdEvent(event));
// Resolve our adsManager.
this.adsManagerResolve();
} }
onAdEvent(event) { onAdEvent(event) {
@ -127,9 +159,58 @@ export default class Ads {
// let intervalTimer; // let intervalTimer;
switch (event.type) { 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 // This is the first event sent for an ad - it is possible to
// determine whether the ad is a video ad or an overlay. // 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. // Show the ad display element.
this.adDisplayElement.style.display = 'block'; this.adDisplayElement.style.display = 'block';
@ -141,59 +222,60 @@ export default class Ads {
ad.width = container.offsetWidth; ad.width = container.offsetWidth;
ad.height = container.offsetHeight; 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 // This event indicates the ad has started - the video player
// can adjust the UI, for example display a pause button and // can adjust the UI, for example display a pause button and
// remaining time. // 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.player.pause();
this.handleEventListeners('STARTED'); 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; break;
case google.ima.AdEvent.Type.DURATION_CHANGE:
case window.google.ima.AdEvent.Type.CONTENT_PAUSE_REQUESTED: this.player.debug.log(`[${(Date.now() - this.time) / 1000}s][IMA SDK] DURATION_CHANGE |`, 'Fired when the ad\'s duration changes.');
this.handleEventListeners('CONTENT_PAUSE_REQUESTED');
break; break;
case google.ima.AdEvent.Type.FIRST_QUARTILE:
case window.google.ima.AdEvent.Type.CONTENT_RESUME_REQUESTED: this.player.debug.log(`[${(Date.now() - this.time) / 1000}s][IMA SDK] FIRST_QUARTILE |`, 'Fired when the ad playhead crosses first quartile.');
this.handleEventListeners('CONTENT_RESUME_REQUESTED');
break; break;
case google.ima.AdEvent.Type.IMPRESSION:
case window.google.ima.AdEvent.Type.AD_BREAK_READY: this.player.debug.log(`[${(Date.now() - this.time) / 1000}s][IMA SDK] IMPRESSION |`, 'Fired when the impression URL has been pinged.');
// 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');
break; break;
case google.ima.AdEvent.Type.INTERACTION:
case window.google.ima.AdEvent.Type.COMPLETE: 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.');
// 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();
}
break; break;
case google.ima.AdEvent.Type.LINEAR_CHANGED:
case window.google.ima.AdEvent.Type.ALL_ADS_COMPLETED: 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.');
this.handleEventListeners('ALL_ADS_COMPLETED'); 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; break;
default: default:
@ -202,21 +284,44 @@ export default class Ads {
} }
onAdError(adErrorEvent) { onAdError(adErrorEvent) {
// Handle the error logging. this.cancel();
this.adDisplayElement.remove();
if (this.adsManager) {
this.adsManager.destroy();
}
if (this.player.debug) { if (this.player.debug) {
throw new Error(adErrorEvent); 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() { setupAdDisplayContainer() {
const { container } = this.player.elements; 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 // We assume the adContainer is the video container of the plyr element
// that will house the ads. // that will house the ads.
this.adDisplayContainer = new window.google.ima.AdDisplayContainer(container); this.adDisplayContainer = new window.google.ima.AdDisplayContainer(container);
@ -230,13 +335,21 @@ export default class Ads {
// Set class name on the adDisplayContainer element. // Set class name on the adDisplayContainer element.
this.adDisplayElement.setAttribute('class', this.player.config.classNames.ads); this.adDisplayElement.setAttribute('class', this.player.config.classNames.ads);
// Play ads when clicked. // 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); this.setOnClickHandler(this.adDisplayElement, this.playAds);
});
} }
playAds() { playAds() {
const { container } = this.player.elements; const { container } = this.player.elements;
// 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. // Initialize the container. Must be done via a user action on mobile devices.
this.adDisplayContainer.initialize(); this.adDisplayContainer.initialize();
@ -256,6 +369,7 @@ export default class Ads {
throw new Error(adError); throw new Error(adError);
} }
} }
});
} }
/** /**
@ -314,7 +428,6 @@ export default class Ads {
* @param {element} element - The element on which to set the listener * @param {element} element - The element on which to set the listener
* @param {function} callback - The callback which will be invoked once triggered. * @param {function} callback - The callback which will be invoked once triggered.
*/ */
setOnClickHandler(element, callback) { setOnClickHandler(element, callback) {
for (let i = 0; i < this.startEvents.length; i += 1) { for (let i = 0; i < this.startEvents.length; i += 1) {
const startEvent = this.startEvents[i]; const startEvent = this.startEvents[i];
@ -339,4 +452,36 @@ export default class Ads {
this.events[event] = callback; this.events[event] = callback;
return this; 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;
}
}
} }