From 12a7a4142cd550e85a65e07d3a1cd8a5bc5d6ffa Mon Sep 17 00:00:00 2001 From: Arthur Hulsman Date: Wed, 17 Jan 2018 11:32:13 +0100 Subject: [PATCH 01/11] Moved the logic for pausing and playing the video to content pause/ resume IMA events to avoid flickering. Also used events for resolving the adsmanager and adsloader promises. --- src/js/plugins/ads.js | 74 +++++++++++++++++++------------------------ 1 file changed, 32 insertions(+), 42 deletions(-) diff --git a/src/js/plugins/ads.js b/src/js/plugins/ads.js index ceb00ee4..87ce61a1 100644 --- a/src/js/plugins/ads.js +++ b/src/js/plugins/ads.js @@ -51,20 +51,20 @@ class Ads { this.events = {}; this.safetyTimer = null; + // Set listeners on the Plyr instance. + this.setupListeners(); + // Setup a simple promise to resolve if the IMA loader is ready. - this.adsLoaderResolve = () => {}; this.adsLoaderPromise = new Promise((resolve) => { - this.adsLoaderResolve = resolve; + this.on('ADS_LOADER_LOADED', () => 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.on('ADS_MANAGER_LOADED', () => resolve()); }); this.adsManagerPromise.then(() => { // Clear the safety timer. @@ -81,9 +81,6 @@ class Ads { // Setup the IMA SDK. this.setupIMA(); - - // Set listeners on the Plyr instance. - this.setupListeners(); } setupIMA() { @@ -109,7 +106,7 @@ class Ads { this.adsLoader.requestAds(adsRequest); - this.adsLoaderResolve(); + this.handleEventListeners('ADS_LOADER_LOADED'); } onAdsManagerLoaded(adsManagerLoadedEvent) { @@ -141,7 +138,7 @@ class Ads { this.adsManager.addEventListener(google.ima.AdEvent.Type.COMPLETE, event => this.onAdEvent(event)); // Resolve our adsManager. - this.adsManagerResolve(); + this.handleEventListeners('ADS_MANAGER_LOADED'); } onAdEvent(event) { @@ -160,12 +157,7 @@ class Ads { switch (event.type) { 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.handleEventListeners('AD_BREAK_READY'); - // this.playing = true; - // this.adsManager.start(); 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.'); @@ -178,29 +170,36 @@ class Ads { 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.player.debug.log(`[${(Date.now() - this.time) / 1000}s][IMA SDK] COMPLETE |`, 'Fired when the ad completes playing.'); + break; + case google.ima.AdEvent.Type.CONTENT_PAUSE_REQUESTED: + // 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] 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'); + + // Show our advertiment container. + this.adsDisplayElement.style.display = 'block'; + + this.playing = true; + + // Pause our video. + this.player.pause(); + break; + case google.ima.AdEvent.Type.CONTENT_RESUME_REQUESTED: // 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.playing = false; - - this.adsDisplayElement.style.display = 'none'; - - if (this.player.currentTime < this.player.duration) { - this.player.play(); - } - break; - case 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'); - this.player.pause(); - break; - - case 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'); + + // Hide the advertisement container. + this.adsDisplayElement.style.display = 'none'; + + this.playing = false; + + // Play our video. if (this.player.currentTime < this.player.duration) { this.player.play(); } @@ -211,9 +210,6 @@ class Ads { this.player.debug.log(`[${(Date.now() - this.time) / 1000}s][IMA SDK] LOADED |`, event.getAd().getContentType()); this.handleEventListeners('LOADED'); - // Show the ad display element. - this.adsDisplayElement.style.display = 'block'; - if (!ad.isLinear()) { // Position AdDisplayContainer correctly for overlay. ad.width = container.offsetWidth; @@ -224,13 +220,7 @@ class Ads { // 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.playing = true; - this.handleEventListeners('STARTED'); break; 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.'); From 9e52296dc62c3efab3e00204ab2e561b712c8965 Mon Sep 17 00:00:00 2001 From: Arthur Hulsman Date: Wed, 17 Jan 2018 12:19:32 +0100 Subject: [PATCH 02/11] Moved the ads container to be outside of the video wrapper. This way we can easily move the ad in front or behind the video controls based on content resume or pause IMA events. --- src/js/plugins/ads.js | 18 +++++++++++++++--- src/sass/plugins/ads.scss | 2 +- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/js/plugins/ads.js b/src/js/plugins/ads.js index 87ce61a1..48a5e4da 100644 --- a/src/js/plugins/ads.js +++ b/src/js/plugins/ads.js @@ -276,7 +276,11 @@ class Ads { } setupAdDisplayContainer() { - const { wrapper } = this.player.elements; + // Create the container for our advertisements. + const container = utils.createElement('div', { + class: this.player.config.classNames.ads, + }); + this.player.elements.container.appendChild(container); // So we can run VPAID2. google.ima.settings.setVpaidMode(google.ima.ImaSdkSettings.VpaidMode.ENABLED); @@ -287,9 +291,9 @@ class Ads { // We assume the adContainer is the video container of the plyr element // that will house the ads. - this.adDisplayContainer = new google.ima.AdDisplayContainer(wrapper); + this.adDisplayContainer = new google.ima.AdDisplayContainer(container); - this.adsDisplayElement = wrapper.firstChild; + this.adsDisplayElement = container.firstChild; // The AdDisplayContainer call from google IMA sets the style attribute // by default. We remove the inline style and set it through the stylesheet. @@ -298,6 +302,14 @@ class Ads { // Set class name on the adDisplayContainer element. this.adsDisplayElement.setAttribute('class', this.player.config.classNames.ads); + // Make sure our advertisement container has the right z-index. + this.on('CONTENT_PAUSE_REQUESTED', () => { + container.style.zIndex = '3'; + }); + this.on('CONTENT_RESUME_REQUESTED', () => { + container.style.zIndex = '1'; + }); + // Play ads when clicked. Wait until the adsManager and adsLoader // are both resolved. Promise.all([ diff --git a/src/sass/plugins/ads.scss b/src/sass/plugins/ads.scss index 41d4d8d1..b6664fe9 100644 --- a/src/sass/plugins/ads.scss +++ b/src/sass/plugins/ads.scss @@ -9,7 +9,7 @@ position: absolute; right: 0; top: 0; - z-index: 10; + z-index: 1; video { left: 0; From d822f0c6bfefddfbce3a987014077de9b68db00a Mon Sep 17 00:00:00 2001 From: Arthur Hulsman Date: Wed, 17 Jan 2018 13:58:39 +0100 Subject: [PATCH 03/11] Adsmanager is now re/pre-loaded with new ads when the video is done or an ad error appears. Will make it possible to request ads when a new video is loaded. Added comments and missing events within the adsmanagerloader method. --- src/js/plugins/ads.js | 353 ++++++++++++++++++++++++++++-------------- 1 file changed, 237 insertions(+), 116 deletions(-) diff --git a/src/js/plugins/ads.js b/src/js/plugins/ads.js index 48a5e4da..8e6d90a0 100644 --- a/src/js/plugins/ads.js +++ b/src/js/plugins/ads.js @@ -42,8 +42,8 @@ class Ads { ready() { this.time = Date.now(); this.startEvents = getStartEvents(); + this.adsContainer = null; this.adDisplayContainer = null; - this.adsDisplayElement = null; this.adsManager = null; this.adsLoader = null; this.adsCuePoints = null; @@ -54,61 +54,125 @@ class Ads { // Set listeners on the Plyr instance. this.setupListeners(); + // Start ticking our safety timer. If the whole advertisement + // thing doesn't resolve within our set time; we bail. + this.startSafetyTimer(12000, 'ready()'); + // Setup a simple promise to resolve if the IMA loader is ready. this.adsLoaderPromise = new Promise((resolve) => { this.on('ADS_LOADER_LOADED', () => 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.adsManagerPromise = new Promise((resolve) => { this.on('ADS_MANAGER_LOADED', () => resolve()); - }); - this.adsManagerPromise.then(() => { + this.player.debug.log(`[${(Date.now() - this.time) / 1000}s][IMA SDK] adsManager resolved!`, this.adsManager); + // 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(); - // Setup the IMA SDK. this.setupIMA(); } + /** + * setupIMA + * In order for the SDK to display ads for our video, we need to tell it + * where to put them, so here we define our ad container. This div is set + * up to render on top of the video player. Using the code below, we tell + * the SDK to render ads within that div. We also provide a handle to the + * content video player - the SDK will poll the current time of our player + * to properly place mid-rolls. After we create the ad display container, + * we initialize it. On mobile devices, this initialization is done as the + * result of a user action. + */ setupIMA() { - const { container } = this.player.elements; + // Create the container for our advertisements. + this.adsContainer = utils.createElement('div', { + class: this.player.config.classNames.ads, + }); + this.player.elements.container.appendChild(this.adsContainer); - // Create ads loader. - this.adsLoader = new google.ima.AdsLoader(this.adDisplayContainer); + // So we can run VPAID2. + google.ima.settings.setVpaidMode(google.ima.ImaSdkSettings.VpaidMode.ENABLED); - // Listen and respond to ads loaded and error events. - this.adsLoader.addEventListener(google.ima.AdsManagerLoadedEvent.Type.ADS_MANAGER_LOADED, event => this.onAdsManagerLoaded(event), false); - this.adsLoader.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR, error => this.onAdError(error), false); + // Set language. + // Todo: Could make a config option out of this locale value. + google.ima.settings.setLocale('en'); - // Request video ads. - const adsRequest = new google.ima.AdsRequest(); - adsRequest.adTagUrl = this.player.config.ads.tagUrl; + // We assume the adContainer is the video container of the plyr element + // that will house the ads. + this.adDisplayContainer = new google.ima.AdDisplayContainer(this.adsContainer); - // Specify the linear and nonlinear slot sizes. This helps the SDK to - // select the correct creative if multiple are returned. - adsRequest.linearAdSlotWidth = container.offsetWidth; - adsRequest.linearAdSlotHeight = container.offsetHeight; - adsRequest.nonLinearAdSlotWidth = container.offsetWidth; - adsRequest.nonLinearAdSlotHeight = container.offsetHeight; + const adsDisplayElement = this.adsContainer.firstChild; - this.adsLoader.requestAds(adsRequest); + // The AdDisplayContainer call from google IMA sets the style attribute + // by default. We remove the inline style and set it through the stylesheet. + adsDisplayElement.removeAttribute('style'); - this.handleEventListeners('ADS_LOADER_LOADED'); + // Set class name on the adDisplayContainer element. + adsDisplayElement.setAttribute('class', this.player.config.classNames.ads); + + // Play ads when clicked. Wait until the adsManager and adsLoader + // are both resolved. + Promise.all([ + this.adsManagerPromise, + this.adsLoaderPromise, + ]).then(() => { + this.setOnClickHandler(adsDisplayElement, this.play); + }); + + // Request video ads to be pre-loaded. + this.requestAds(); } + /** + * Request advertisements. + */ + requestAds() { + const { container } = this.player.elements; + + try { + // Create ads loader. + this.adsLoader = new google.ima.AdsLoader(this.adDisplayContainer); + + // Listen and respond to ads loaded and error events. + this.adsLoader.addEventListener( + google.ima.AdsManagerLoadedEvent.Type.ADS_MANAGER_LOADED, + event => this.onAdsManagerLoaded(event), false); + this.adsLoader.addEventListener( + google.ima.AdErrorEvent.Type.AD_ERROR, + error => this.onAdError(error), false); + + // Request video ads. + const adsRequest = new google.ima.AdsRequest(); + adsRequest.adTagUrl = this.player.config.ads.tagUrl; + + // Specify the linear and nonlinear slot sizes. This helps the SDK + // to select the correct creative if multiple are returned. + adsRequest.linearAdSlotWidth = container.offsetWidth; + adsRequest.linearAdSlotHeight = container.offsetHeight; + adsRequest.nonLinearAdSlotWidth = container.offsetWidth; + adsRequest.nonLinearAdSlotHeight = container.offsetHeight; + + // We only overlay ads as we only support video. + adsRequest.forceNonLinearFullSlot = false; + + this.adsLoader.requestAds(adsRequest); + + this.handleEventListeners('ADS_LOADER_LOADED'); + } catch (e) { + this.onAdError(e); + } + } + + /** + * This method is called whenever the ads are ready inside + * the AdDisplayContainer. + * @param {Event} adsManagerLoadedEvent + */ onAdsManagerLoaded(adsManagerLoadedEvent) { // Get the ads manager. @@ -126,21 +190,44 @@ class Ads { this.adsCuePoints = this.adsManager.getCuePoints(); // Add listeners to the required events. + // Advertisement error events. this.adsManager.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR, error => this.onAdError(error)); + + // Advertisement regular events. + this.adsManager.addEventListener(google.ima.AdEvent.Type.AD_BREAK_READY, event => this.onAdEvent(event)); + this.adsManager.addEventListener(google.ima.AdEvent.Type.AD_METADATA, event => this.onAdEvent(event)); + this.adsManager.addEventListener(google.ima.AdEvent.Type.ALL_ADS_COMPLETED, event => this.onAdEvent(event)); + this.adsManager.addEventListener(google.ima.AdEvent.Type.CLICK, event => this.onAdEvent(event)); + this.adsManager.addEventListener(google.ima.AdEvent.Type.COMPLETE, event => this.onAdEvent(event)); this.adsManager.addEventListener(google.ima.AdEvent.Type.CONTENT_PAUSE_REQUESTED, event => this.onAdEvent(event)); this.adsManager.addEventListener(google.ima.AdEvent.Type.CONTENT_RESUME_REQUESTED, event => this.onAdEvent(event)); - this.adsManager.addEventListener(google.ima.AdEvent.Type.ALL_ADS_COMPLETED, event => this.onAdEvent(event)); - this.adsManager.addEventListener(google.ima.AdEvent.Type.AD_BREAK_READY, event => this.onAdEvent(event)); - - // Listen to any additional events, if necessary. this.adsManager.addEventListener(google.ima.AdEvent.Type.LOADED, event => this.onAdEvent(event)); this.adsManager.addEventListener(google.ima.AdEvent.Type.STARTED, event => this.onAdEvent(event)); - this.adsManager.addEventListener(google.ima.AdEvent.Type.COMPLETE, event => this.onAdEvent(event)); + this.adsManager.addEventListener(google.ima.AdEvent.Type.DURATION_CHANGE, event => this.onAdEvent(event)); + this.adsManager.addEventListener(google.ima.AdEvent.Type.FIRST_QUARTILE, event => this.onAdEvent(event)); + this.adsManager.addEventListener(google.ima.AdEvent.Type.IMPRESSION, event => this.onAdEvent(event)); + this.adsManager.addEventListener(google.ima.AdEvent.Type.INTERACTION, event => this.onAdEvent(event)); + this.adsManager.addEventListener(google.ima.AdEvent.Type.LINEAR_CHANGED, event => this.onAdEvent(event)); + this.adsManager.addEventListener(google.ima.AdEvent.Type.MIDPOINT, event => this.onAdEvent(event)); + this.adsManager.addEventListener(google.ima.AdEvent.Type.PAUSED, event => this.onAdEvent(event)); + this.adsManager.addEventListener(google.ima.AdEvent.Type.RESUMED, event => this.onAdEvent(event)); + this.adsManager.addEventListener(google.ima.AdEvent.Type.SKIPPABLE_STATE_CHANGED, event => this.onAdEvent(event)); + this.adsManager.addEventListener(google.ima.AdEvent.Type.SKIPPED, event => this.onAdEvent(event)); + this.adsManager.addEventListener(google.ima.AdEvent.Type.THIRD_QUARTILE, event => this.onAdEvent(event)); + this.adsManager.addEventListener(google.ima.AdEvent.Type.USER_CLOSE, event => this.onAdEvent(event)); + this.adsManager.addEventListener(google.ima.AdEvent.Type.VOLUME_CHANGED, event => this.onAdEvent(event)); + this.adsManager.addEventListener(google.ima.AdEvent.Type.VOLUME_MUTED, event => this.onAdEvent(event)); // Resolve our adsManager. this.handleEventListeners('ADS_MANAGER_LOADED'); } + /** + * This is where all the event handling takes place. Retrieve the ad from + * the event. Some events (e.g. ALL_ADS_COMPLETED) don't have ad + * object associated. + * @param {Event} event + */ onAdEvent(event) { const { container } = this.player.elements; @@ -163,8 +250,45 @@ class Ads { 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: + // All ads for the current videos are done. We can now + // request new advertisements in case the video is re-played. 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'); + + // Todo: Example for what happens when a next video in a playlist would be loaded. + // So here we load a new video when all ads are done. + // Then we load new ads within a new adsManager. When the video + // Is started - after - the ads are loaded, then we get ads. + // You can also easily test cancelling and reloading by running + // player.ads.cancel() and player.ads.play from the console I guess. + // this.player.source = { + // type: 'video', + // title: 'View From A Blue Moon', + // sources: [{ + // src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.mp4', + // type: 'video/mp4', + // }], + // poster: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.jpg', + // tracks: [ + // { + // kind: 'captions', + // label: 'English', + // srclang: 'en', + // src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.en.vtt', + // default: true, + // }, + // { + // kind: 'captions', + // label: 'French', + // srclang: 'fr', + // src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.fr.vtt', + // }, + // ], + // }; + + // Todo: So there is still this thing where a video should only be allowed to start playing when the IMA SDK is ready or has failed. + + this.loadAds(); 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.'); @@ -178,14 +302,7 @@ class Ads { // remaining time. 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'); - - // Show our advertiment container. - this.adsDisplayElement.style.display = 'block'; - - this.playing = true; - - // Pause our video. - this.player.pause(); + this.contentPause(); break; case google.ima.AdEvent.Type.CONTENT_RESUME_REQUESTED: // This event indicates the ad has finished - the video player @@ -193,16 +310,7 @@ class Ads { // remaining time detection. 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'); - - // Hide the advertisement container. - this.adsDisplayElement.style.display = 'none'; - - this.playing = false; - - // Play our video. - if (this.player.currentTime < this.player.duration) { - this.player.play(); - } + this.contentResume(); break; case google.ima.AdEvent.Type.LOADED: // This is the first event sent for an ad - it is possible to @@ -270,56 +378,15 @@ class Ads { } } + /** + * Any ad error handling comes through here. + * @param {Event} adErrorEvent + */ onAdError(adErrorEvent) { this.cancel(); this.player.debug.log(`[${(Date.now() - this.time) / 1000}s][IMA SDK] ERROR |`, adErrorEvent); } - setupAdDisplayContainer() { - // Create the container for our advertisements. - const container = utils.createElement('div', { - class: this.player.config.classNames.ads, - }); - this.player.elements.container.appendChild(container); - - // 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 google.ima.AdDisplayContainer(container); - - this.adsDisplayElement = container.firstChild; - - // The AdDisplayContainer call from google IMA sets the style attribute - // by default. We remove the inline style and set it through the stylesheet. - this.adsDisplayElement.removeAttribute('style'); - - // Set class name on the adDisplayContainer element. - this.adsDisplayElement.setAttribute('class', this.player.config.classNames.ads); - - // Make sure our advertisement container has the right z-index. - this.on('CONTENT_PAUSE_REQUESTED', () => { - container.style.zIndex = '3'; - }); - this.on('CONTENT_RESUME_REQUESTED', () => { - container.style.zIndex = '1'; - }); - - // Play ads when clicked. Wait until the adsManager and adsLoader - // are both resolved. - Promise.all([ - this.adsManagerPromise, - this.adsLoaderPromise, - ]).then(() => { - this.setOnClickHandler(this.adsDisplayElement, this.play); - }); - } - /** * Setup hooks for Plyr and window events. This ensures * the mid- and post-roll launch at the correct time. And @@ -380,17 +447,49 @@ class Ads { this.initialized = true; } catch (adError) { - // An error may be thrown if there was a problem with the VAST response. - this.adsDisplayElement.remove(); - - if (this.player.debug) { - throw new Error(adError); - } - this.player.play(); + // An error may be thrown if there was a problem with the + // VAST response. + this.onAdError(adError); } }); } + /** + * Resume our video. + */ + contentResume() { + this.player.debug.log(`[${(Date.now() - this.time) / 1000}s][IMA SDK]`, 'Resume video.'); + + // Hide our ad container. + this.adsContainer.style.display = 'none'; + this.adsContainer.style.zIndex = '1'; + + // Ad is stopped. + this.playing = false; + + // Play our video. + if (this.player.currentTime < this.player.duration) { + this.player.play(); + } + } + + /** + * Pause our video. + */ + contentPause() { + this.player.debug.log(`[${(Date.now() - this.time) / 1000}s][IMA SDK]`, 'Pause video.'); + + // Show our ad container. + this.adsContainer.style.display = 'block'; + this.adsContainer.style.zIndex = '3'; + + // Ad is playing. + this.playing = true; + + // Pause our video. + this.player.pause(); + } + /** * 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 @@ -401,14 +500,40 @@ class Ads { 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.adsDisplayElement.remove(); + // Pause our video. + this.contentResume(); + + // Tell our instance that we're done for now. + this.handleEventListeners('ERROR'); + + // Re-create our adsManager. + this.loadAds(); + } + + /** + * Re-create our adsManager. + */ + loadAds() { + this.player.debug.log(`[${(Date.now() - this.time) / 1000}s][IMA SDK]`, 'Re-loading advertisements.'); // Tell our adsManager to go bye bye. this.adsManagerPromise.then(() => { + // Destroy our adsManager. if (this.adsManager) { this.adsManager.destroy(); } + + // Re-set our adsManager promises. + this.adsManagerPromise = new Promise((resolve) => { + this.on('ADS_MANAGER_LOADED', () => resolve()); + this.player.debug.log(`[${(Date.now() - this.time) / 1000}s][IMA SDK] adsManager resolved!`, this.adsManager); + }); + + // Make sure we can re-call advertisements. + this.initialized = false; + + // Now request some new advertisements. + this.requestAds(); }); } @@ -453,15 +578,12 @@ class Ads { } /** - * 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. + * Setup a safety timer for when the ad network doesn't respond for + * whatever reason. The advertisement has 12 seconds to get its things + * 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}`); @@ -472,9 +594,8 @@ class Ads { } /** - * clearSafetyTimer + * Clear our safety timer(s). * @param {String} from - * @private */ clearSafetyTimer(from) { if (typeof this.safetyTimer !== 'undefined' && this.safetyTimer !== null) { From 3583165b30ac80afbd6235fc2febd1a5987d4f8b Mon Sep 17 00:00:00 2001 From: Arthur Hulsman Date: Wed, 17 Jan 2018 14:09:11 +0100 Subject: [PATCH 04/11] Removed logic related to starting the ad by clicking/ tapping the advertisement container. Ad is started by plyr play method. --- src/js/plugins/ads.js | 60 --------------------------------------- src/sass/plugins/ads.scss | 3 +- 2 files changed, 2 insertions(+), 61 deletions(-) diff --git a/src/js/plugins/ads.js b/src/js/plugins/ads.js index 8e6d90a0..3b76fcc8 100644 --- a/src/js/plugins/ads.js +++ b/src/js/plugins/ads.js @@ -1,23 +1,5 @@ import utils from '../utils'; -// Events are different on various devices. We set the correct events, based on userAgent. -const getStartEvents = () => { - let events = ['click']; - - // TODO: Detecting touch is tricky, we should look at other ways? - // For mobile users the start event will be one of - // touchstart, touchend and touchmove. - if (navigator.userAgent.match(/iPhone/i) || navigator.userAgent.match(/iPad/i) || navigator.userAgent.match(/Android/i)) { - events = [ - 'touchstart', - 'touchend', - 'touchmove', - ]; - } - - return events; -}; - class Ads { constructor(player) { this.player = player; @@ -41,7 +23,6 @@ class Ads { ready() { this.time = Date.now(); - this.startEvents = getStartEvents(); this.adsContainer = null; this.adDisplayContainer = null; this.adsManager = null; @@ -106,24 +87,6 @@ class Ads { // that will house the ads. this.adDisplayContainer = new google.ima.AdDisplayContainer(this.adsContainer); - const adsDisplayElement = this.adsContainer.firstChild; - - // The AdDisplayContainer call from google IMA sets the style attribute - // by default. We remove the inline style and set it through the stylesheet. - adsDisplayElement.removeAttribute('style'); - - // Set class name on the adDisplayContainer element. - adsDisplayElement.setAttribute('class', this.player.config.classNames.ads); - - // Play ads when clicked. Wait until the adsManager and adsLoader - // are both resolved. - Promise.all([ - this.adsManagerPromise, - this.adsLoaderPromise, - ]).then(() => { - this.setOnClickHandler(adsDisplayElement, this.play); - }); - // Request video ads to be pre-loaded. this.requestAds(); } @@ -462,7 +425,6 @@ class Ads { // Hide our ad container. this.adsContainer.style.display = 'none'; - this.adsContainer.style.zIndex = '1'; // Ad is stopped. this.playing = false; @@ -481,7 +443,6 @@ class Ads { // Show our ad container. this.adsContainer.style.display = 'block'; - this.adsContainer.style.zIndex = '3'; // Ad is playing. this.playing = true; @@ -546,27 +507,6 @@ class Ads { } } - /** - * Set start event listener on a DOM element and triggers the - * callback when clicked. - * @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]; - element.addEventListener( - startEvent, - event => { - if ((event.type === 'touchend' && startEvent === 'touchend') || event.type === 'click') { - callback.call(this); - } - }, - { once: true }, - ); - } - } - /** * Add event listeners * @param {string} event - Event type diff --git a/src/sass/plugins/ads.scss b/src/sass/plugins/ads.scss index b6664fe9..3c91dd5e 100644 --- a/src/sass/plugins/ads.scss +++ b/src/sass/plugins/ads.scss @@ -3,13 +3,14 @@ // ========================================================================== .plyr__ads { + display: none; // Hide initially. bottom: 0; cursor: pointer; left: 0; position: absolute; right: 0; top: 0; - z-index: 1; + z-index: 3; // Above the controls. video { left: 0; From 1d1eb02bd734461bad8884c827e79c05d8f77dbd Mon Sep 17 00:00:00 2001 From: Arthur Hulsman Date: Wed, 17 Jan 2018 14:44:44 +0100 Subject: [PATCH 05/11] Added the logging of our main promises to their resolving callback. Otherwise they come up as null. --- src/js/plugins/ads.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/js/plugins/ads.js b/src/js/plugins/ads.js index 3b76fcc8..3738ed81 100644 --- a/src/js/plugins/ads.js +++ b/src/js/plugins/ads.js @@ -42,12 +42,16 @@ class Ads { // Setup a simple promise to resolve if the IMA loader is ready. this.adsLoaderPromise = new Promise((resolve) => { this.on('ADS_LOADER_LOADED', () => 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.adsManagerPromise = new Promise((resolve) => { this.on('ADS_MANAGER_LOADED', () => resolve()); + }); + this.adsManagerPromise.then(() => { this.player.debug.log(`[${(Date.now() - this.time) / 1000}s][IMA SDK] adsManager resolved!`, this.adsManager); // Clear the safety timer. From 896ea7c689f25e5eed5ad6bcac0e9314cff825ed Mon Sep 17 00:00:00 2001 From: Arthur Hulsman Date: Wed, 17 Jan 2018 15:38:26 +0100 Subject: [PATCH 06/11] Added cue markings within the time line for when midrolls will be displayed. Removed unusued callback parameter. --- src/js/defaults.js | 1 + src/js/plugins/ads.js | 17 +++++++++++++++-- src/sass/plugins/ads.scss | 14 ++++++++++++++ 3 files changed, 30 insertions(+), 2 deletions(-) diff --git a/src/js/defaults.js b/src/js/defaults.js index 8f1d37b1..d893c43c 100644 --- a/src/js/defaults.js +++ b/src/js/defaults.js @@ -312,6 +312,7 @@ const defaults = { error: 'plyr--has-error', hover: 'plyr--hover', tooltip: 'plyr__tooltip', + cues: 'plyr__cues', hidden: 'plyr__sr-only', hideControls: 'plyr--hide-controls', isIos: 'plyr--is-ios', diff --git a/src/js/plugins/ads.js b/src/js/plugins/ads.js index 3738ed81..1df56b7b 100644 --- a/src/js/plugins/ads.js +++ b/src/js/plugins/ads.js @@ -141,6 +141,7 @@ class Ads { * @param {Event} adsManagerLoadedEvent */ onAdsManagerLoaded(adsManagerLoadedEvent) { + const { container } = this.player.elements; // Get the ads manager. const settings = new google.ima.AdsRenderingSettings(); @@ -156,6 +157,18 @@ class Ads { // Get the cue points for any mid-rolls by filtering out the pre- and post-roll. this.adsCuePoints = this.adsManager.getCuePoints(); + // Add advertisement cue's within the time line if available. + this.adsCuePoints.forEach((cuePoint, index) => { + if (cuePoint !== 0 && cuePoint !== -1) { + const seekElement = this.player.elements.progress; + const cue = utils.createElement('span', { + class: this.player.config.classNames.cues, + }); + cue.style.left = cuePoint.toString() + 'px'; + seekElement.appendChild(cue); + } + }); + // Add listeners to the required events. // Advertisement error events. this.adsManager.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR, error => this.onAdError(error)); @@ -368,12 +381,12 @@ class Ads { this.adsLoader.contentComplete(); }); - this.player.on('seeking', event => { + this.player.on('seeking', () => { time = this.player.currentTime; return time; }); - this.player.on('seeked', event => { + this.player.on('seeked', () => { const seekedTime = this.player.currentTime; this.adsCuePoints.forEach((cuePoint, index) => { diff --git a/src/sass/plugins/ads.scss b/src/sass/plugins/ads.scss index 3c91dd5e..4bff7a20 100644 --- a/src/sass/plugins/ads.scss +++ b/src/sass/plugins/ads.scss @@ -16,3 +16,17 @@ left: 0; } } + +// Advertisement cue's for the progress bar. +.plyr__cues { + display: block; + position: absolute; + z-index: 3; // Between progress and thumb. + top: 50%; + left: 0; + margin: -($plyr-range-track-height / 2) 0 0; + width: 3px; + height: $plyr-range-track-height; + background: currentColor; + opacity: 0.8; +} From 0cb2f9588845dc7cf93c3bec45968100c2db8166 Mon Sep 17 00:00:00 2001 From: Arthur Hulsman Date: Wed, 17 Jan 2018 15:43:08 +0100 Subject: [PATCH 07/11] Removed an un-used variable. --- src/js/plugins/ads.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/js/plugins/ads.js b/src/js/plugins/ads.js index 1df56b7b..dcd236a9 100644 --- a/src/js/plugins/ads.js +++ b/src/js/plugins/ads.js @@ -141,8 +141,6 @@ class Ads { * @param {Event} adsManagerLoadedEvent */ onAdsManagerLoaded(adsManagerLoadedEvent) { - const { container } = this.player.elements; - // Get the ads manager. const settings = new google.ima.AdsRenderingSettings(); @@ -158,7 +156,7 @@ class Ads { this.adsCuePoints = this.adsManager.getCuePoints(); // Add advertisement cue's within the time line if available. - this.adsCuePoints.forEach((cuePoint, index) => { + this.adsCuePoints.forEach((cuePoint) => { if (cuePoint !== 0 && cuePoint !== -1) { const seekElement = this.player.elements.progress; const cue = utils.createElement('span', { From 31c816656267efa495d02e7f3429d35d91f9c648 Mon Sep 17 00:00:00 2001 From: Arthur Hulsman Date: Wed, 17 Jan 2018 15:57:10 +0100 Subject: [PATCH 08/11] Fixed string literal and position issue of the midroll cue inside the time line. Added a check for the progress element existence. --- src/js/plugins/ads.js | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/js/plugins/ads.js b/src/js/plugins/ads.js index dcd236a9..0b12a8e7 100644 --- a/src/js/plugins/ads.js +++ b/src/js/plugins/ads.js @@ -159,11 +159,14 @@ class Ads { this.adsCuePoints.forEach((cuePoint) => { if (cuePoint !== 0 && cuePoint !== -1) { const seekElement = this.player.elements.progress; - const cue = utils.createElement('span', { - class: this.player.config.classNames.cues, - }); - cue.style.left = cuePoint.toString() + 'px'; - seekElement.appendChild(cue); + if(seekElement) { + const cuePercentage = 100 / this.player.duration * cuePoint; + const cue = utils.createElement('span', { + class: this.player.config.classNames.cues, + }); + cue.style.left = `${cuePercentage.toString()}%`; + seekElement.appendChild(cue); + } } }); From d87ada4f58c71299f38ed2764da04a77312faf93 Mon Sep 17 00:00:00 2001 From: Arthur Hulsman Date: Thu, 18 Jan 2018 12:26:53 +0100 Subject: [PATCH 09/11] Reformatted ads codebase and added/ changed comments. Also removed un-used events. --- src/js/plugins/ads.js | 283 ++++++++++++++++++------------------------ 1 file changed, 119 insertions(+), 164 deletions(-) diff --git a/src/js/plugins/ads.js b/src/js/plugins/ads.js index 0b12a8e7..bdd07d34 100644 --- a/src/js/plugins/ads.js +++ b/src/js/plugins/ads.js @@ -1,6 +1,14 @@ import utils from '../utils'; +/** + * Advertisements using Google IMA HTML5 SDK. + */ class Ads { + /** + * Ads constructor. + * @param {object} player + * @return {Ads} + */ constructor(player) { this.player = player; this.playing = false; @@ -21,6 +29,9 @@ class Ads { } } + /** + * Get the ads instance ready. + */ ready() { this.time = Date.now(); this.adsContainer = null; @@ -28,7 +39,6 @@ class Ads { this.adsManager = null; this.adsLoader = null; this.adsCuePoints = null; - this.currentAd = null; this.events = {}; this.safetyTimer = null; @@ -44,7 +54,7 @@ class Ads { this.on('ADS_LOADER_LOADED', () => resolve()); }); this.adsLoaderPromise.then(() => { - this.player.debug.log(`[${(Date.now() - this.time) / 1000}s][IMA SDK] adsLoader resolved!`, this.adsLoader); + this.player.debug.log('Ads loader resolved!', this.adsLoader); }); // Setup a promise to resolve if the IMA manager is ready. @@ -52,7 +62,7 @@ class Ads { this.on('ADS_MANAGER_LOADED', () => resolve()); }); this.adsManagerPromise.then(() => { - this.player.debug.log(`[${(Date.now() - this.time) / 1000}s][IMA SDK] adsManager resolved!`, this.adsManager); + this.player.debug.log('Ads manager resolved!', this.adsManager); // Clear the safety timer. this.clearSafetyTimer('onAdsManagerLoaded()'); @@ -63,15 +73,12 @@ class Ads { } /** - * setupIMA - * In order for the SDK to display ads for our video, we need to tell it - * where to put them, so here we define our ad container. This div is set - * up to render on top of the video player. Using the code below, we tell - * the SDK to render ads within that div. We also provide a handle to the - * content video player - the SDK will poll the current time of our player - * to properly place mid-rolls. After we create the ad display container, - * we initialize it. On mobile devices, this initialization is done as the - * result of a user action. + * In order for the SDK to display ads for our video, we need to tell it where to put them, + * so here we define our ad container. This div is set up to render on top of the video player. + * Using the code below, we tell the SDK to render ads within that div. We also provide a + * handle to the content video player - the SDK will poll the current time of our player to + * properly place mid-rolls. After we create the ad display container, we initialize it. On + * mobile devices, this initialization is done as the result of a user action. */ setupIMA() { // Create the container for our advertisements. @@ -136,8 +143,7 @@ class Ads { } /** - * This method is called whenever the ads are ready inside - * the AdDisplayContainer. + * This method is called whenever the ads are ready inside the AdDisplayContainer. * @param {Event} adsManagerLoadedEvent */ onAdsManagerLoaded(adsManagerLoadedEvent) { @@ -159,7 +165,7 @@ class Ads { this.adsCuePoints.forEach((cuePoint) => { if (cuePoint !== 0 && cuePoint !== -1) { const seekElement = this.player.elements.progress; - if(seekElement) { + if (seekElement) { const cuePercentage = 100 / this.player.duration * cuePoint; const cue = utils.createElement('span', { class: this.player.config.classNames.cues, @@ -172,68 +178,80 @@ class Ads { // Add listeners to the required events. // Advertisement error events. - this.adsManager.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR, error => this.onAdError(error)); + this.adsManager.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR, + error => this.onAdError(error)); // Advertisement regular events. - this.adsManager.addEventListener(google.ima.AdEvent.Type.AD_BREAK_READY, event => this.onAdEvent(event)); - this.adsManager.addEventListener(google.ima.AdEvent.Type.AD_METADATA, event => this.onAdEvent(event)); - this.adsManager.addEventListener(google.ima.AdEvent.Type.ALL_ADS_COMPLETED, event => this.onAdEvent(event)); - this.adsManager.addEventListener(google.ima.AdEvent.Type.CLICK, event => this.onAdEvent(event)); - this.adsManager.addEventListener(google.ima.AdEvent.Type.COMPLETE, event => this.onAdEvent(event)); - this.adsManager.addEventListener(google.ima.AdEvent.Type.CONTENT_PAUSE_REQUESTED, event => this.onAdEvent(event)); - this.adsManager.addEventListener(google.ima.AdEvent.Type.CONTENT_RESUME_REQUESTED, event => this.onAdEvent(event)); - this.adsManager.addEventListener(google.ima.AdEvent.Type.LOADED, event => this.onAdEvent(event)); - this.adsManager.addEventListener(google.ima.AdEvent.Type.STARTED, event => this.onAdEvent(event)); - this.adsManager.addEventListener(google.ima.AdEvent.Type.DURATION_CHANGE, event => this.onAdEvent(event)); - this.adsManager.addEventListener(google.ima.AdEvent.Type.FIRST_QUARTILE, event => this.onAdEvent(event)); - this.adsManager.addEventListener(google.ima.AdEvent.Type.IMPRESSION, event => this.onAdEvent(event)); - this.adsManager.addEventListener(google.ima.AdEvent.Type.INTERACTION, event => this.onAdEvent(event)); - this.adsManager.addEventListener(google.ima.AdEvent.Type.LINEAR_CHANGED, event => this.onAdEvent(event)); - this.adsManager.addEventListener(google.ima.AdEvent.Type.MIDPOINT, event => this.onAdEvent(event)); - this.adsManager.addEventListener(google.ima.AdEvent.Type.PAUSED, event => this.onAdEvent(event)); - this.adsManager.addEventListener(google.ima.AdEvent.Type.RESUMED, event => this.onAdEvent(event)); - this.adsManager.addEventListener(google.ima.AdEvent.Type.SKIPPABLE_STATE_CHANGED, event => this.onAdEvent(event)); - this.adsManager.addEventListener(google.ima.AdEvent.Type.SKIPPED, event => this.onAdEvent(event)); - this.adsManager.addEventListener(google.ima.AdEvent.Type.THIRD_QUARTILE, event => this.onAdEvent(event)); - this.adsManager.addEventListener(google.ima.AdEvent.Type.USER_CLOSE, event => this.onAdEvent(event)); - this.adsManager.addEventListener(google.ima.AdEvent.Type.VOLUME_CHANGED, event => this.onAdEvent(event)); - this.adsManager.addEventListener(google.ima.AdEvent.Type.VOLUME_MUTED, event => this.onAdEvent(event)); + this.adsManager.addEventListener(google.ima.AdEvent.Type.AD_BREAK_READY, + event => this.onAdEvent(event)); + this.adsManager.addEventListener(google.ima.AdEvent.Type.AD_METADATA, + event => this.onAdEvent(event)); + this.adsManager.addEventListener(google.ima.AdEvent.Type.ALL_ADS_COMPLETED, + event => this.onAdEvent(event)); + this.adsManager.addEventListener(google.ima.AdEvent.Type.CLICK, + event => this.onAdEvent(event)); + this.adsManager.addEventListener(google.ima.AdEvent.Type.COMPLETE, + event => this.onAdEvent(event)); + this.adsManager.addEventListener(google.ima.AdEvent.Type.CONTENT_PAUSE_REQUESTED, + event => this.onAdEvent(event)); + this.adsManager.addEventListener(google.ima.AdEvent.Type.CONTENT_RESUME_REQUESTED, + event => this.onAdEvent(event)); + this.adsManager.addEventListener(google.ima.AdEvent.Type.LOADED, + event => this.onAdEvent(event)); + this.adsManager.addEventListener(google.ima.AdEvent.Type.STARTED, + event => this.onAdEvent(event)); + this.adsManager.addEventListener(google.ima.AdEvent.Type.DURATION_CHANGE, + event => this.onAdEvent(event)); + this.adsManager.addEventListener(google.ima.AdEvent.Type.FIRST_QUARTILE, + event => this.onAdEvent(event)); + this.adsManager.addEventListener(google.ima.AdEvent.Type.IMPRESSION, + event => this.onAdEvent(event)); + this.adsManager.addEventListener(google.ima.AdEvent.Type.INTERACTION, + event => this.onAdEvent(event)); + this.adsManager.addEventListener(google.ima.AdEvent.Type.LINEAR_CHANGED, + event => this.onAdEvent(event)); + this.adsManager.addEventListener(google.ima.AdEvent.Type.MIDPOINT, + event => this.onAdEvent(event)); + this.adsManager.addEventListener(google.ima.AdEvent.Type.PAUSED, + event => this.onAdEvent(event)); + this.adsManager.addEventListener(google.ima.AdEvent.Type.RESUMED, + event => this.onAdEvent(event)); + this.adsManager.addEventListener(google.ima.AdEvent.Type.SKIPPABLE_STATE_CHANGED, + event => this.onAdEvent(event)); + this.adsManager.addEventListener(google.ima.AdEvent.Type.SKIPPED, + event => this.onAdEvent(event)); + this.adsManager.addEventListener(google.ima.AdEvent.Type.THIRD_QUARTILE, + event => this.onAdEvent(event)); + this.adsManager.addEventListener(google.ima.AdEvent.Type.USER_CLOSE, + event => this.onAdEvent(event)); + this.adsManager.addEventListener(google.ima.AdEvent.Type.VOLUME_CHANGED, + event => this.onAdEvent(event)); + this.adsManager.addEventListener(google.ima.AdEvent.Type.VOLUME_MUTED, + event => this.onAdEvent(event)); // Resolve our adsManager. this.handleEventListeners('ADS_MANAGER_LOADED'); } /** - * This is where all the event handling takes place. Retrieve the ad from - * the event. Some events (e.g. ALL_ADS_COMPLETED) don't have ad - * object associated. + * This is where all the event handling takes place. Retrieve the ad from the event. Some + * events (e.g. ALL_ADS_COMPLETED) don't have the ad object associated. * @param {Event} event */ onAdEvent(event) { const { container } = this.player.elements; + // Listen for events if debugging. + this.player.debug.log(`ads event: ${event.type}`); + // Retrieve the ad from the event. Some events (e.g. ALL_ADS_COMPLETED) // don't have ad object associated. const ad = event.getAd(); - // Set the currently played ad. This information could be used by callback - // events. - this.currentAd = ad; - - // let intervalTimer; - switch (event.type) { - - case google.ima.AdEvent.Type.AD_BREAK_READY: - 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.'); - 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: - // All ads for the current videos are done. We can now - // request new advertisements in case the video is re-played. - 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.'); + // All ads for the current videos are done. We can now request new advertisements + // in case the video is re-played. this.handleEventListeners('ALL_ADS_COMPLETED'); // Todo: Example for what happens when a next video in a playlist would be loaded. @@ -246,57 +264,39 @@ class Ads { // type: 'video', // title: 'View From A Blue Moon', // sources: [{ - // src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.mp4', - // type: 'video/mp4', - // }], - // poster: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.jpg', - // tracks: [ - // { - // kind: 'captions', - // label: 'English', - // srclang: 'en', - // src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.en.vtt', - // default: true, - // }, - // { - // kind: 'captions', - // label: 'French', - // srclang: 'fr', - // src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.fr.vtt', - // }, - // ], + // src: + // 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.mp4', type: + // 'video/mp4', }], poster: + // 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.jpg', tracks: + // [ { kind: 'captions', label: 'English', srclang: 'en', src: + // 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.en.vtt', + // default: true, }, { kind: 'captions', label: 'French', srclang: 'fr', src: + // 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.fr.vtt', }, ], // }; - // Todo: So there is still this thing where a video should only be allowed to start playing when the IMA SDK is ready or has failed. + // Todo: So there is still this thing where a video should only be allowed to start + // playing when the IMA SDK is ready or has failed. this.loadAds(); 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.player.debug.log(`[${(Date.now() - this.time) / 1000}s][IMA SDK] COMPLETE |`, 'Fired when the ad completes playing.'); - break; case google.ima.AdEvent.Type.CONTENT_PAUSE_REQUESTED: - // 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] CONTENT_PAUSE_REQUESTED |`, 'Fired when content should be paused. This usually happens right before an ad is about to cover the content.'); + // This event indicates the ad has started - the video player can adjust the UI, + // for example display a pause button and remaining time. Fired when content should + // be paused. This usually happens right before an ad is about to cover the content. this.handleEventListeners('CONTENT_PAUSE_REQUESTED'); - this.contentPause(); + this.pause(); break; case google.ima.AdEvent.Type.CONTENT_RESUME_REQUESTED: - // This event indicates the ad has finished - the video player - // can perform appropriate UI actions, such as removing the timer for - // remaining time detection. - 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 event indicates the ad has finished - the video player can perform + // appropriate UI actions, such as removing the timer for remaining time detection. + // Fired when content should be resumed. This usually happens when an ad finishes + // or collapses. this.handleEventListeners('CONTENT_RESUME_REQUESTED'); - this.contentResume(); + this.resume(); 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()); + // 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.handleEventListeners('LOADED'); if (!ad.isLinear()) { @@ -308,51 +308,6 @@ class Ads { // console.info('Ad type: ' + event.getAd().getAdPodInfo().getPodIndex()); // console.info('Ad time: ' + event.getAd().getAdPodInfo().getTimeOffset()); break; - case google.ima.AdEvent.Type.STARTED: - this.player.debug.log(`[${(Date.now() - this.time) / 1000}s][IMA SDK] STARTED |`, 'Fired when the ad starts playing.'); - break; - 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 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 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 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 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: break; @@ -365,7 +320,7 @@ class Ads { */ onAdError(adErrorEvent) { this.cancel(); - this.player.debug.log(`[${(Date.now() - this.time) / 1000}s][IMA SDK] ERROR |`, adErrorEvent); + this.player.debug.log('Ads error.', adErrorEvent); } /** @@ -400,7 +355,8 @@ class Ads { // Listen to the resizing of the window. And resize ad accordingly. window.addEventListener('resize', () => { - this.adsManager.resize(container.offsetWidth, container.offsetHeight, google.ima.ViewMode.NORMAL); + this.adsManager.resize(container.offsetWidth, container.offsetHeight, + google.ima.ViewMode.NORMAL); }); } @@ -419,7 +375,8 @@ class Ads { if (!this.initialized) { // Initialize the ads manager. Ad rules playlist will start at this time. - this.adsManager.init(container.offsetWidth, container.offsetHeight, google.ima.ViewMode.NORMAL); + this.adsManager.init(container.offsetWidth, container.offsetHeight, + 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. @@ -438,8 +395,8 @@ class Ads { /** * Resume our video. */ - contentResume() { - this.player.debug.log(`[${(Date.now() - this.time) / 1000}s][IMA SDK]`, 'Resume video.'); + resume() { + this.player.debug.log('Resume video.'); // Hide our ad container. this.adsContainer.style.display = 'none'; @@ -456,8 +413,8 @@ class Ads { /** * Pause our video. */ - contentPause() { - this.player.debug.log(`[${(Date.now() - this.time) / 1000}s][IMA SDK]`, 'Pause video.'); + pause() { + this.player.debug.log('Pause video.'); // Show our ad container. this.adsContainer.style.display = 'block'; @@ -470,17 +427,16 @@ class Ads { } /** - * 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 + * 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.'); + this.player.debug.warn('Ad cancelled.'); // Pause our video. - this.contentResume(); + this.resume(); // Tell our instance that we're done for now. this.handleEventListeners('ERROR'); @@ -493,8 +449,6 @@ class Ads { * Re-create our adsManager. */ loadAds() { - this.player.debug.log(`[${(Date.now() - this.time) / 1000}s][IMA SDK]`, 'Re-loading advertisements.'); - // Tell our adsManager to go bye bye. this.adsManagerPromise.then(() => { // Destroy our adsManager. @@ -505,7 +459,7 @@ class Ads { // Re-set our adsManager promises. this.adsManagerPromise = new Promise((resolve) => { this.on('ADS_MANAGER_LOADED', () => resolve()); - this.player.debug.log(`[${(Date.now() - this.time) / 1000}s][IMA SDK] adsManager resolved!`, this.adsManager); + this.player.debug.log(this.adsManager); }); // Make sure we can re-call advertisements. @@ -518,6 +472,7 @@ class Ads { /** * Handles callbacks after an ad event was invoked. + * @param {string} event - Event type */ handleEventListeners(event) { if (typeof this.events[event] !== 'undefined') { @@ -529,6 +484,7 @@ class Ads { * Add event listeners * @param {string} event - Event type * @param {function} callback - Callback for when event occurs + * @return {Ads} */ on(event, callback) { this.events[event] = callback; @@ -536,15 +492,15 @@ class Ads { } /** - * Setup a safety timer for when the ad network doesn't respond for - * whatever reason. The advertisement has 12 seconds to get its things - * 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. + * Setup a safety timer for when the ad network doesn't respond for whatever reason. + * The advertisement has 12 seconds to get its things 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 */ startSafetyTimer(time, from) { - this.player.debug.log(`[${(Date.now() - this.time) / 1000}s][IMA SDK]`, `Safety timer invoked timer from: ${from}`); + this.player.debug.log(`Safety timer invoked from: ${from}.`); this.safetyTimer = window.setTimeout(() => { this.cancel(); this.clearSafetyTimer('startSafetyTimer()'); @@ -557,7 +513,7 @@ class Ads { */ 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}`); + this.player.debug.log(`Safety timer cleared from: ${from}.`); clearTimeout(this.safetyTimer); this.safetyTimer = undefined; } @@ -565,4 +521,3 @@ class Ads { } export default Ads; - From 8af312fe3cfe55c5003b645f1fcfe23601fc435f Mon Sep 17 00:00:00 2001 From: Arthur Hulsman Date: Thu, 18 Jan 2018 12:31:10 +0100 Subject: [PATCH 10/11] Updated pause and resume content methods within Ads class. --- src/js/plugins/ads.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/js/plugins/ads.js b/src/js/plugins/ads.js index bdd07d34..2e7b1e93 100644 --- a/src/js/plugins/ads.js +++ b/src/js/plugins/ads.js @@ -284,7 +284,7 @@ class Ads { // for example display a pause button and remaining time. Fired when content should // be paused. This usually happens right before an ad is about to cover the content. this.handleEventListeners('CONTENT_PAUSE_REQUESTED'); - this.pause(); + this.pauseContent(); break; case google.ima.AdEvent.Type.CONTENT_RESUME_REQUESTED: // This event indicates the ad has finished - the video player can perform @@ -292,7 +292,7 @@ class Ads { // Fired when content should be resumed. This usually happens when an ad finishes // or collapses. this.handleEventListeners('CONTENT_RESUME_REQUESTED'); - this.resume(); + this.resumeContent(); break; case google.ima.AdEvent.Type.LOADED: // This is the first event sent for an ad - it is possible to determine whether the @@ -395,7 +395,7 @@ class Ads { /** * Resume our video. */ - resume() { + resumeContent() { this.player.debug.log('Resume video.'); // Hide our ad container. @@ -413,7 +413,7 @@ class Ads { /** * Pause our video. */ - pause() { + pauseContent() { this.player.debug.log('Pause video.'); // Show our ad container. @@ -436,7 +436,7 @@ class Ads { this.player.debug.warn('Ad cancelled.'); // Pause our video. - this.resume(); + this.resumeContent(); // Tell our instance that we're done for now. this.handleEventListeners('ERROR'); From ed6048034b86a1de7a705daf28150b79c45def20 Mon Sep 17 00:00:00 2001 From: Arthur Hulsman Date: Thu, 18 Jan 2018 14:04:47 +0100 Subject: [PATCH 11/11] Noticed that Plyr stopped working when ads are blocked. --- src/js/plugins/ads.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/js/plugins/ads.js b/src/js/plugins/ads.js index 2e7b1e93..4e405962 100644 --- a/src/js/plugins/ads.js +++ b/src/js/plugins/ads.js @@ -33,7 +33,6 @@ class Ads { * Get the ads instance ready. */ ready() { - this.time = Date.now(); this.adsContainer = null; this.adDisplayContainer = null; this.adsManager = null; @@ -366,11 +365,15 @@ class Ads { play() { const { container } = this.player.elements; - // Initialize the container. Must be done via a user action on mobile devices. - this.adDisplayContainer.initialize(); + if (!this.adsManagerPromise) { + return; + } // 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 { if (!this.initialized) {