diff options
Diffstat (limited to 'src/js/plugins/ads.js')
-rw-r--r-- | src/js/plugins/ads.js | 1114 |
1 files changed, 561 insertions, 553 deletions
diff --git a/src/js/plugins/ads.js b/src/js/plugins/ads.js index 6b4fca10..1a52ebce 100644 --- a/src/js/plugins/ads.js +++ b/src/js/plugins/ads.js @@ -11,626 +11,634 @@ import { triggerEvent } from '../utils/events'; import i18n from '../utils/i18n'; import is from '../utils/is'; import loadScript from '../utils/load-script'; +import { silencePromise } from '../utils/promise'; import { formatTime } from '../utils/time'; import { buildUrlParams } from '../utils/urls'; const destroy = instance => { - // Destroy our adsManager - if (instance.manager) { - instance.manager.destroy(); - } + // Destroy our adsManager + if (instance.manager) { + instance.manager.destroy(); + } - // Destroy our adsManager - if (instance.elements.displayContainer) { - instance.elements.displayContainer.destroy(); - } + // Destroy our adsManager + if (instance.elements.displayContainer) { + instance.elements.displayContainer.destroy(); + } - instance.elements.container.remove(); + instance.elements.container.remove(); }; class Ads { - /** - * Ads constructor. - * @param {Object} player - * @return {Ads} - */ - constructor(player) { - this.player = player; - this.config = player.config.ads; - this.playing = false; - this.initialized = false; - this.elements = { - container: null, - displayContainer: null, - }; - this.manager = null; - this.loader = null; - this.cuePoints = null; - this.events = {}; - this.safetyTimer = null; - this.countdownTimer = null; - - // Setup a promise to resolve when the IMA manager is ready - this.managerPromise = new Promise((resolve, reject) => { - // The ad is loaded and ready - this.on('loaded', resolve); - - // Ads failed - this.on('error', reject); - }); - - this.load(); + /** + * Ads constructor. + * @param {Object} player + * @return {Ads} + */ + constructor(player) { + this.player = player; + this.config = player.config.ads; + this.playing = false; + this.initialized = false; + this.elements = { + container: null, + displayContainer: null, + }; + this.manager = null; + this.loader = null; + this.cuePoints = null; + this.events = {}; + this.safetyTimer = null; + this.countdownTimer = null; + + // Setup a promise to resolve when the IMA manager is ready + this.managerPromise = new Promise((resolve, reject) => { + // The ad is loaded and ready + this.on('loaded', resolve); + + // Ads failed + this.on('error', reject); + }); + + this.load(); + } + + get enabled() { + const { config } = this; + + return ( + this.player.isHTML5 && + this.player.isVideo && + config.enabled && + (!is.empty(config.publisherId) || is.url(config.tagUrl)) + ); + } + + /** + * Load the IMA SDK + */ + load() { + if (!this.enabled) { + return; } - get enabled() { - const { config } = this; - - return ( - this.player.isHTML5 && - this.player.isVideo && - config.enabled && - (!is.empty(config.publisherId) || is.url(config.tagUrl)) - ); - } - - /** - * Load the IMA SDK - */ - load() { - if (!this.enabled) { - return; - } - - // Check if the Google IMA3 SDK is loaded or load it ourselves - if (!is.object(window.google) || !is.object(window.google.ima)) { - loadScript(this.player.config.urls.googleIMA.sdk) - .then(() => { - this.ready(); - }) - .catch(() => { - // Script failed to load or is blocked - this.trigger('error', new Error('Google IMA SDK failed to load')); - }); - } else { - this.ready(); - } - } - - /** - * Get the ads instance ready - */ - ready() { - // Double check we're enabled - if (!this.enabled) { - destroy(this); - } - - // Start ticking our safety timer. If the whole advertisement - // thing doesn't resolve within our set time; we bail - this.startSafetyTimer(12000, 'ready()'); - - // Clear the safety timer - this.managerPromise.then(() => { - this.clearSafetyTimer('onAdsManagerLoaded()'); + // Check if the Google IMA3 SDK is loaded or load it ourselves + if (!is.object(window.google) || !is.object(window.google.ima)) { + loadScript(this.player.config.urls.googleIMA.sdk) + .then(() => { + this.ready(); + }) + .catch(() => { + // Script failed to load or is blocked + this.trigger('error', new Error('Google IMA SDK failed to load')); }); - - // Set listeners on the Plyr instance - this.listeners(); - - // Setup the IMA SDK - this.setupIMA(); + } else { + this.ready(); } - - // Build the tag URL - get tagUrl() { - const { config } = this; - - if (is.url(config.tagUrl)) { - return config.tagUrl; - } - - const params = { - AV_PUBLISHERID: '58c25bb0073ef448b1087ad6', - AV_CHANNELID: '5a0458dc28a06145e4519d21', - AV_URL: window.location.hostname, - cb: Date.now(), - AV_WIDTH: 640, - AV_HEIGHT: 480, - AV_CDIM2: config.publisherId, - }; - - const base = 'https://go.aniview.com/api/adserver6/vast/'; - - return `${base}?${buildUrlParams(params)}`; + } + + /** + * Get the ads instance ready + */ + ready() { + // Double check we're enabled + if (!this.enabled) { + destroy(this); } - /** - * 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 - this.elements.container = createElement('div', { - class: this.player.config.classNames.ads, - }); - - this.player.elements.container.appendChild(this.elements.container); + // Start ticking our safety timer. If the whole advertisement + // thing doesn't resolve within our set time; we bail + this.startSafetyTimer(12000, 'ready()'); - // So we can run VPAID2 - google.ima.settings.setVpaidMode(google.ima.ImaSdkSettings.VpaidMode.ENABLED); + // Clear the safety timer + this.managerPromise.then(() => { + this.clearSafetyTimer('onAdsManagerLoaded()'); + }); - // Set language - google.ima.settings.setLocale(this.player.config.ads.language); + // Set listeners on the Plyr instance + this.listeners(); - // Set playback for iOS10+ - google.ima.settings.setDisableCustomPlaybackForIOS10Plus(this.player.config.playsinline); + // Setup the IMA SDK + this.setupIMA(); + } - // We assume the adContainer is the video container of the plyr element that will house the ads - this.elements.displayContainer = new google.ima.AdDisplayContainer(this.elements.container, this.player.media); + // Build the tag URL + get tagUrl() { + const { config } = this; - // Request video ads to be pre-loaded - this.requestAds(); + if (is.url(config.tagUrl)) { + return config.tagUrl; } - /** - * Request advertisements - */ - requestAds() { - const { container } = this.player.elements; - - try { - // Create ads loader - this.loader = new google.ima.AdsLoader(this.elements.displayContainer); - - // Listen and respond to ads loaded and error events - this.loader.addEventListener( - google.ima.AdsManagerLoadedEvent.Type.ADS_MANAGER_LOADED, - event => this.onAdsManagerLoaded(event), - false, - ); - this.loader.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR, error => this.onAdError(error), false); - - // Request video ads - const request = new google.ima.AdsRequest(); - request.adTagUrl = this.tagUrl; - - // Specify the linear and nonlinear slot sizes. This helps the SDK - // to select the correct creative if multiple are returned - request.linearAdSlotWidth = container.offsetWidth; - request.linearAdSlotHeight = container.offsetHeight; - request.nonLinearAdSlotWidth = container.offsetWidth; - request.nonLinearAdSlotHeight = container.offsetHeight; - - // We only overlay ads as we only support video. - request.forceNonLinearFullSlot = false; - - // Mute based on current state - request.setAdWillPlayMuted(!this.player.muted); - - this.loader.requestAds(request); - } catch (e) { - this.onAdError(e); - } + const params = { + AV_PUBLISHERID: '58c25bb0073ef448b1087ad6', + AV_CHANNELID: '5a0458dc28a06145e4519d21', + AV_URL: window.location.hostname, + cb: Date.now(), + AV_WIDTH: 640, + AV_HEIGHT: 480, + AV_CDIM2: config.publisherId, + }; + + const base = 'https://go.aniview.com/api/adserver6/vast/'; + + return `${base}?${buildUrlParams(params)}`; + } + + /** + * 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 + this.elements.container = createElement('div', { + class: this.player.config.classNames.ads, + }); + + this.player.elements.container.appendChild(this.elements.container); + + // So we can run VPAID2 + google.ima.settings.setVpaidMode(google.ima.ImaSdkSettings.VpaidMode.ENABLED); + + // Set language + google.ima.settings.setLocale(this.player.config.ads.language); + + // Set playback for iOS10+ + google.ima.settings.setDisableCustomPlaybackForIOS10Plus(this.player.config.playsinline); + + // We assume the adContainer is the video container of the plyr element that will house the ads + this.elements.displayContainer = new google.ima.AdDisplayContainer(this.elements.container, this.player.media); + + // Create ads loader + this.loader = new google.ima.AdsLoader(this.elements.displayContainer); + + // Listen and respond to ads loaded and error events + this.loader.addEventListener( + google.ima.AdsManagerLoadedEvent.Type.ADS_MANAGER_LOADED, + event => this.onAdsManagerLoaded(event), + false, + ); + this.loader.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR, error => this.onAdError(error), false); + + // Request video ads to be pre-loaded + this.requestAds(); + } + + /** + * Request advertisements + */ + requestAds() { + const { container } = this.player.elements; + + try { + // Request video ads + const request = new google.ima.AdsRequest(); + request.adTagUrl = this.tagUrl; + + // Specify the linear and nonlinear slot sizes. This helps the SDK + // to select the correct creative if multiple are returned + request.linearAdSlotWidth = container.offsetWidth; + request.linearAdSlotHeight = container.offsetHeight; + request.nonLinearAdSlotWidth = container.offsetWidth; + request.nonLinearAdSlotHeight = container.offsetHeight; + + // We only overlay ads as we only support video. + request.forceNonLinearFullSlot = false; + + // Mute based on current state + request.setAdWillPlayMuted(!this.player.muted); + + this.loader.requestAds(request); + } catch (e) { + this.onAdError(e); + } + } + + /** + * Update the ad countdown + * @param {Boolean} start + */ + pollCountdown(start = false) { + if (!start) { + clearInterval(this.countdownTimer); + this.elements.container.removeAttribute('data-badge-text'); + return; } - /** - * Update the ad countdown - * @param {Boolean} start - */ - pollCountdown(start = false) { - if (!start) { - clearInterval(this.countdownTimer); - this.elements.container.removeAttribute('data-badge-text'); - return; - } - - const update = () => { - const time = formatTime(Math.max(this.manager.getRemainingTime(), 0)); - const label = `${i18n.get('advertisement', this.player.config)} - ${time}`; - this.elements.container.setAttribute('data-badge-text', label); - }; - - this.countdownTimer = setInterval(update, 100); + const update = () => { + const time = formatTime(Math.max(this.manager.getRemainingTime(), 0)); + const label = `${i18n.get('advertisement', this.player.config)} - ${time}`; + this.elements.container.setAttribute('data-badge-text', label); + }; + + this.countdownTimer = setInterval(update, 100); + } + + /** + * This method is called whenever the ads are ready inside the AdDisplayContainer + * @param {Event} adsManagerLoadedEvent + */ + onAdsManagerLoaded(event) { + // Load could occur after a source change (race condition) + if (!this.enabled) { + return; } - /** - * This method is called whenever the ads are ready inside the AdDisplayContainer - * @param {Event} adsManagerLoadedEvent - */ - onAdsManagerLoaded(event) { - // Load could occur after a source change (race condition) - if (!this.enabled) { - return; - } + // Get the ads manager + const settings = new google.ima.AdsRenderingSettings(); - // Get the ads manager - const settings = new google.ima.AdsRenderingSettings(); + // Tell the SDK to save and restore content video state on our behalf + settings.restoreCustomPlaybackStateOnAdBreakComplete = true; + settings.enablePreloading = true; - // Tell the SDK to save and restore content video state on our behalf - settings.restoreCustomPlaybackStateOnAdBreakComplete = true; - settings.enablePreloading = true; + // The SDK is polling currentTime on the contentPlayback. And needs a duration + // so it can determine when to start the mid- and post-roll + this.manager = event.getAdsManager(this.player, settings); - // The SDK is polling currentTime on the contentPlayback. And needs a duration - // so it can determine when to start the mid- and post-roll - this.manager = event.getAdsManager(this.player, settings); + // Get the cue points for any mid-rolls by filtering out the pre- and post-roll + this.cuePoints = this.manager.getCuePoints(); - // Get the cue points for any mid-rolls by filtering out the pre- and post-roll - this.cuePoints = this.manager.getCuePoints(); + // Add listeners to the required events + // Advertisement error events + this.manager.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR, error => this.onAdError(error)); - // Add listeners to the required events - // Advertisement error events - this.manager.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR, error => this.onAdError(error)); + // Advertisement regular events + Object.keys(google.ima.AdEvent.Type).forEach(type => { + this.manager.addEventListener(google.ima.AdEvent.Type[type], e => this.onAdEvent(e)); + }); - // Advertisement regular events - Object.keys(google.ima.AdEvent.Type).forEach(type => { - this.manager.addEventListener(google.ima.AdEvent.Type[type], e => this.onAdEvent(e)); - }); + // Resolve our adsManager + this.trigger('loaded'); + } - // Resolve our adsManager - this.trigger('loaded'); - } + addCuePoints() { + // Add advertisement cue's within the time line if available + if (!is.empty(this.cuePoints)) { + this.cuePoints.forEach(cuePoint => { + if (cuePoint !== 0 && cuePoint !== -1 && cuePoint < this.player.duration) { + const seekElement = this.player.elements.progress; - addCuePoints() { - // Add advertisement cue's within the time line if available - if (!is.empty(this.cuePoints)) { - this.cuePoints.forEach(cuePoint => { - if (cuePoint !== 0 && cuePoint !== -1 && cuePoint < this.player.duration) { - const seekElement = this.player.elements.progress; - - if (is.element(seekElement)) { - const cuePercentage = (100 / this.player.duration) * cuePoint; - const cue = createElement('span', { - class: this.player.config.classNames.cues, - }); - - cue.style.left = `${cuePercentage.toString()}%`; - seekElement.appendChild(cue); - } - } + if (is.element(seekElement)) { + const cuePercentage = (100 / this.player.duration) * cuePoint; + const cue = createElement('span', { + class: this.player.config.classNames.cues, }); - } - } - /** - * 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 - * https://developers.google.com/interactive-media-ads/docs/sdks/html5/v3/apis#ima.AdEvent.Type - * @param {Event} event - */ - onAdEvent(event) { - const { container } = this.player.elements; - // Retrieve the ad from the event. Some events (e.g. ALL_ADS_COMPLETED) - // don't have ad object associated - const ad = event.getAd(); - const adData = event.getAdData(); - - // Proxy event - const dispatchEvent = type => { - triggerEvent.call(this.player, this.player.media, `ads${type.replace(/_/g, '').toLowerCase()}`); - }; - - // Bubble the event - dispatchEvent(event.type); - - switch (event.type) { - 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.trigger('loaded'); - - // Start countdown - this.pollCountdown(true); - - if (!ad.isLinear()) { - // Position AdDisplayContainer correctly for overlay - ad.width = container.offsetWidth; - ad.height = container.offsetHeight; - } - - // console.info('Ad type: ' + event.getAd().getAdPodInfo().getPodIndex()); - // console.info('Ad time: ' + event.getAd().getAdPodInfo().getTimeOffset()); - - break; - - case google.ima.AdEvent.Type.STARTED: - // Set volume to match player - this.manager.setVolume(this.player.volume); - - 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 - - // 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.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. Fired when content should - // be paused. This usually happens right before an ad is about to cover the content - - this.pauseContent(); - - 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. - // Fired when content should be resumed. This usually happens when an ad finishes - // or collapses - - this.pollCountdown(); - - this.resumeContent(); - - break; - - case google.ima.AdEvent.Type.LOG: - if (adData.adError) { - this.player.debug.warn(`Non-fatal ad error: ${adData.adError.getMessage()}`); - } - - break; - - default: - break; + cue.style.left = `${cuePercentage.toString()}%`; + seekElement.appendChild(cue); + } } + }); } + } + + /** + * 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 + * https://developers.google.com/interactive-media-ads/docs/sdks/html5/v3/apis#ima.AdEvent.Type + * @param {Event} event + */ + onAdEvent(event) { + const { container } = this.player.elements; + // Retrieve the ad from the event. Some events (e.g. ALL_ADS_COMPLETED) + // don't have ad object associated + const ad = event.getAd(); + const adData = event.getAdData(); + + // Proxy event + const dispatchEvent = type => { + triggerEvent.call(this.player, this.player.media, `ads${type.replace(/_/g, '').toLowerCase()}`); + }; + + // Bubble the event + dispatchEvent(event.type); + + switch (event.type) { + 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.trigger('loaded'); - /** - * Any ad error handling comes through here - * @param {Event} event - */ - onAdError(event) { - this.cancel(); - this.player.debug.warn('Ads error', event); - } - - /** - * Setup hooks for Plyr and window events. This ensures - * the mid- and post-roll launch at the correct time. And - * resize the advertisement when the player resizes - */ - listeners() { - const { container } = this.player.elements; - let time; - - this.player.on('canplay', () => { - this.addCuePoints(); - }); + // Start countdown + this.pollCountdown(true); - this.player.on('ended', () => { - this.loader.contentComplete(); - }); + if (!ad.isLinear()) { + // Position AdDisplayContainer correctly for overlay + ad.width = container.offsetWidth; + ad.height = container.offsetHeight; + } - this.player.on('timeupdate', () => { - time = this.player.currentTime; - }); + // console.info('Ad type: ' + event.getAd().getAdPodInfo().getPodIndex()); + // console.info('Ad time: ' + event.getAd().getAdPodInfo().getTimeOffset()); + + break; + + case google.ima.AdEvent.Type.STARTED: + // Set volume to match player + this.manager.setVolume(this.player.volume); + + 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 + + // 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 + + if (this.player.ended) { + this.loadAds(); + } else { + // The SDK won't allow new ads to be called without receiving a contentComplete() + this.loader.contentComplete(); + } - this.player.on('seeked', () => { - const seekedTime = this.player.currentTime; + break; - if (is.empty(this.cuePoints)) { - return; - } + 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. Fired when content should + // be paused. This usually happens right before an ad is about to cover the content - this.cuePoints.forEach((cuePoint, index) => { - if (time < cuePoint && cuePoint < seekedTime) { - this.manager.discardAdBreak(); - this.cuePoints.splice(index, 1); - } - }); - }); + this.pauseContent(); - // Listen to the resizing of the window. And resize ad accordingly - // TODO: eventually implement ResizeObserver - window.addEventListener('resize', () => { - if (this.manager) { - this.manager.resize(container.offsetWidth, container.offsetHeight, google.ima.ViewMode.NORMAL); - } - }); - } + break; - /** - * Initialize the adsManager and start playing advertisements - */ - play() { - const { container } = this.player.elements; + 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. + // Fired when content should be resumed. This usually happens when an ad finishes + // or collapses - if (!this.managerPromise) { - this.resumeContent(); - } + this.pollCountdown(); - // Play the requested advertisement whenever the adsManager is ready - this.managerPromise - .then(() => { - // Set volume to match player - this.manager.setVolume(this.player.volume); - - // Initialize the container. Must be done via a user action on mobile devices - this.elements.displayContainer.initialize(); - - try { - if (!this.initialized) { - // Initialize the ads manager. Ad rules playlist will start at this time - this.manager.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 - this.manager.start(); - } - - this.initialized = true; - } catch (adError) { - // An error may be thrown if there was a problem with the - // VAST response - this.onAdError(adError); - } - }) - .catch(() => {}); - } + this.resumeContent(); - /** - * Resume our video - */ - resumeContent() { - // Hide the advertisement container - this.elements.container.style.zIndex = ''; + break; - // Ad is stopped - this.playing = false; - - // Play video - this.player.media.play(); - } - - /** - * Pause our video - */ - pauseContent() { - // Show the advertisement container - this.elements.container.style.zIndex = 3; + case google.ima.AdEvent.Type.LOG: + if (adData.adError) { + this.player.debug.warn(`Non-fatal ad error: ${adData.adError.getMessage()}`); + } - // Ad is playing - this.playing = true; + break; - // Pause our video. - this.player.media.pause(); + default: + break; } - - /** - * 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() { - // Pause our video - if (this.initialized) { - this.resumeContent(); + } + + /** + * Any ad error handling comes through here + * @param {Event} event + */ + onAdError(event) { + this.cancel(); + this.player.debug.warn('Ads error', event); + } + + /** + * Setup hooks for Plyr and window events. This ensures + * the mid- and post-roll launch at the correct time. And + * resize the advertisement when the player resizes + */ + listeners() { + const { container } = this.player.elements; + let time; + + this.player.on('canplay', () => { + this.addCuePoints(); + }); + + this.player.on('ended', () => { + this.loader.contentComplete(); + }); + + this.player.on('timeupdate', () => { + time = this.player.currentTime; + }); + + this.player.on('seeked', () => { + const seekedTime = this.player.currentTime; + + if (is.empty(this.cuePoints)) { + return; + } + + this.cuePoints.forEach((cuePoint, index) => { + if (time < cuePoint && cuePoint < seekedTime) { + this.manager.discardAdBreak(); + this.cuePoints.splice(index, 1); } - - // Tell our instance that we're done for now - this.trigger('error'); - - // Re-create our adsManager - this.loadAds(); + }); + }); + + // Listen to the resizing of the window. And resize ad accordingly + // TODO: eventually implement ResizeObserver + window.addEventListener('resize', () => { + if (this.manager) { + this.manager.resize(container.offsetWidth, container.offsetHeight, google.ima.ViewMode.NORMAL); + } + }); + } + + /** + * Initialize the adsManager and start playing advertisements + */ + play() { + const { container } = this.player.elements; + + if (!this.managerPromise) { + this.resumeContent(); } - /** - * Re-create our adsManager - */ - loadAds() { - // Tell our adsManager to go bye bye - this.managerPromise - .then(() => { - // Destroy our adsManager - if (this.manager) { - this.manager.destroy(); - } - - // Re-set our adsManager promises - this.managerPromise = new Promise(resolve => { - this.on('loaded', resolve); - this.player.debug.log(this.manager); - }); - - // Now request some new advertisements - this.requestAds(); - }) - .catch(() => {}); - } + // Play the requested advertisement whenever the adsManager is ready + this.managerPromise + .then(() => { + // Set volume to match player + this.manager.setVolume(this.player.volume); - /** - * Handles callbacks after an ad event was invoked - * @param {String} event - Event type - */ - trigger(event, ...args) { - const handlers = this.events[event]; - - if (is.array(handlers)) { - handlers.forEach(handler => { - if (is.function(handler)) { - handler.apply(this, args); - } - }); + // Initialize the container. Must be done via a user action on mobile devices + this.elements.displayContainer.initialize(); + + try { + if (!this.initialized) { + // Initialize the ads manager. Ad rules playlist will start at this time + this.manager.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 + this.manager.start(); + } + + this.initialized = true; + } catch (adError) { + // An error may be thrown if there was a problem with the + // VAST response + this.onAdError(adError); } + }) + .catch(() => {}); + } + + /** + * Resume our video + */ + resumeContent() { + // Hide the advertisement container + this.elements.container.style.zIndex = ''; + + // Ad is stopped + this.playing = false; + + // Play video + silencePromise(this.player.media.play()); + } + + /** + * Pause our video + */ + pauseContent() { + // Show the advertisement container + this.elements.container.style.zIndex = 3; + + // Ad is playing + this.playing = true; + + // Pause our video. + this.player.media.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 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() { + // Pause our video + if (this.initialized) { + this.resumeContent(); } - /** - * Add event listeners - * @param {String} event - Event type - * @param {Function} callback - Callback for when event occurs - * @return {Ads} - */ - on(event, callback) { - if (!is.array(this.events[event])) { - this.events[event] = []; + // Tell our instance that we're done for now + this.trigger('error'); + + // Re-create our adsManager + this.loadAds(); + } + + /** + * Re-create our adsManager + */ + loadAds() { + // Tell our adsManager to go bye bye + this.managerPromise + .then(() => { + // Destroy our adsManager + if (this.manager) { + this.manager.destroy(); } - this.events[event].push(callback); + // Re-set our adsManager promises + this.managerPromise = new Promise(resolve => { + this.on('loaded', resolve); + this.player.debug.log(this.manager); + }); + // Now that the manager has been destroyed set it to also be un-initialized + this.initialized = false; - return this; + // Now request some new advertisements + this.requestAds(); + }) + .catch(() => {}); + } + + /** + * Handles callbacks after an ad event was invoked + * @param {String} event - Event type + */ + trigger(event, ...args) { + const handlers = this.events[event]; + + if (is.array(handlers)) { + handlers.forEach(handler => { + if (is.function(handler)) { + handler.apply(this, args); + } + }); } - - /** - * 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(`Safety timer invoked from: ${from}`); - - this.safetyTimer = setTimeout(() => { - this.cancel(); - this.clearSafetyTimer('startSafetyTimer()'); - }, time); + } + + /** + * Add event listeners + * @param {String} event - Event type + * @param {Function} callback - Callback for when event occurs + * @return {Ads} + */ + on(event, callback) { + if (!is.array(this.events[event])) { + this.events[event] = []; } - /** - * Clear our safety timer(s) - * @param {String} from - */ - clearSafetyTimer(from) { - if (!is.nullOrUndefined(this.safetyTimer)) { - this.player.debug.log(`Safety timer cleared from: ${from}`); - - clearTimeout(this.safetyTimer); - this.safetyTimer = null; - } + this.events[event].push(callback); + + return this; + } + + /** + * 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(`Safety timer invoked from: ${from}`); + + this.safetyTimer = setTimeout(() => { + this.cancel(); + this.clearSafetyTimer('startSafetyTimer()'); + }, time); + } + + /** + * Clear our safety timer(s) + * @param {String} from + */ + clearSafetyTimer(from) { + if (!is.nullOrUndefined(this.safetyTimer)) { + this.player.debug.log(`Safety timer cleared from: ${from}`); + + clearTimeout(this.safetyTimer); + this.safetyTimer = null; } + } } export default Ads; |