diff --git a/.eslintrc.js b/.eslintrc.js index de07900..745f6cf 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -14,7 +14,6 @@ module.exports = { 'no-console': 0, 'no-unused-vars': 1, 'import/no-extraneous-dependencies': ['error', { devDependencies: true, optionalDependencies: true, peerDependencies: true }], - 'import/no-cycle': [2, { maxDepth: 1 }], 'import/prefer-default-export': 0, 'no-param-reassign': ['error', { props: false }], }, diff --git a/src/features/index.js b/src/features/index.js deleted file mode 100644 index b497d86..0000000 --- a/src/features/index.js +++ /dev/null @@ -1,2 +0,0 @@ -export * from './icons'; -export * from './vmap'; diff --git a/src/features/vmap.js b/src/features/vmap.js deleted file mode 100644 index 8302e4f..0000000 --- a/src/features/vmap.js +++ /dev/null @@ -1,70 +0,0 @@ -import { VASTParser } from '@dailymotion/vast-client'; -import Vast from '../index'; -import { fetchVmapUrl } from '../lib'; - -export function parseInlineVastData(vastAdData, adType) { - const xmlString = (new XMLSerializer()).serializeToString(vastAdData); - const vastXml = (new window.DOMParser()).parseFromString(xmlString, 'text/xml'); - const vastParser = new VASTParser(); - vastParser.parseVAST(vastXml) - .then((parsedVAST) => { - if (adType === 'postroll') { - // store for later use (in readyforpostroll event) - this.postRollData = parsedVAST.ads ?? []; - } else if (adType === 'preroll') { - this.adsArray = parsedVAST.ads ?? []; - this.player.trigger('adsready'); - } else if (adType === 'midroll') { - // store for later use (in readyforpostroll event) - this.adsArray = parsedVAST.ads ?? []; - this.readAd(); - } - }) - .catch((err) => { - console.log('error', err); - if (adType === 'postroll' || adType === 'midroll') { - this.disablePostroll(); - } else if (adType === 'preroll') { - // skip preroll, go ahaed to regular content - this.player.ads.skipLinearAdMode(); - } - }); -} - -export async function handleVMAP(vmapUrl) { - try { - const vmap = await fetchVmapUrl(vmapUrl); - if (vmap.adBreaks && vmap.adBreaks.length > 0) { - this.addEventsListeners(); - // handle preroll - const preroll = Vast.getPreroll(vmap.adBreaks); - if (!preroll) { - this.disablePreroll(); - } else if (preroll.adSource?.adTagURI?.uri) { - // load vast preroll url - await this.handleVAST(preroll.adSource.adTagURI.uri); - // a preroll has been found, trigger adsready - this.player.trigger('adsready'); - } else if (preroll.adSource.vastAdData) { - this.parseInlineVastData(preroll.adSource?.vastAdData, 'preroll'); - } - // handle postroll - const postroll = Vast.getPostroll(vmap.adBreaks); - if (!postroll) { - this.disablePostroll(); - } else if (postroll.adSource?.adTagURI?.uri) { - this.postRollUrl = postroll.adSource.adTagURI.uri; - } else if (postroll.adSource?.vastAdData) { - this.parseInlineVastData(postroll.adSource?.vastAdData, 'postroll'); - } - this.watchForProgress = Vast.getMidrolls(vmap.adBreaks); - if (this.watchForProgress.length > 0) { - // listen on regular content for midroll handling - this.player.on('timeupdate', this.onProgress); - } - } - } catch (err) { - // could not fetch vmap - console.error(err); - } -} diff --git a/src/index.js b/src/index.js index 0f46c01..e040e95 100644 --- a/src/index.js +++ b/src/index.js @@ -1,11 +1,16 @@ import videojs from 'video.js'; import 'videojs-contrib-ads'; -import { VASTClient, VASTTracker } from '@dailymotion/vast-client'; +import { VASTClient, VASTTracker, VASTParser } from '@dailymotion/vast-client'; +import { addIcons } from './features/icons'; +import { playLinearAd } from './modes/linear'; +import { playCompanionAd } from './modes/companions'; +import { playNonLinearAd } from './modes/nonlinear'; import { - injectScriptTag, getLocalISOString, convertTimeOffsetToSeconds, + injectScriptTag, getLocalISOString, convertTimeOffsetToSeconds, fetchVmapUrl, } from './lib'; -import { playLinearAd, playNonLinearAd, playCompanionAd } from './modes'; -import { addIcons, handleVMAP, parseInlineVastData } from './features'; +import { + getMidrolls, getPostroll, getPreroll, getBestCtaUrl, +} from './lib/utils'; const Plugin = videojs.getPlugin('plugin'); @@ -147,7 +152,7 @@ class Vast extends Plugin { const currentAd = this.getNextAd(); const linearCreative = currentAd.linearCreative(); // Retrieve the CTA URl to render - this.ctaUrl = Vast.getBestCtaUrl(linearCreative); + this.ctaUrl = getBestCtaUrl(currentAd.linearCreative()); this.debug('ctaUrl', this.ctaUrl); if (currentAd.hasLinearCreative()) { @@ -611,103 +616,6 @@ class Vast extends Plugin { }); }; - static getCloseButton(clickCallback) { - const closeButton = document.createElement('button'); - closeButton.addEventListener('click', clickCallback); - closeButton.style.width = '20px'; - closeButton.style.height = '20px'; - closeButton.style.position = 'absolute'; - closeButton.style.right = '5px'; - closeButton.style.top = '5px'; - closeButton.style.zIndex = '3'; - closeButton.style.background = '#CCC'; - closeButton.style.color = '#000'; - closeButton.style.fontSize = '12px'; - closeButton.style.cursor = 'pointer'; - closeButton.textContent = 'X'; - return closeButton; - } - - static applyNonLinearCommonDomStyle(domElement) { - domElement.style.cursor = 'pointer'; - domElement.style.left = '50%'; - domElement.style.position = 'absolute'; - domElement.style.transform = 'translateX(-50%)'; - domElement.style.bottom = '80px'; - domElement.style.display = 'block'; - domElement.style.zIndex = '2'; - } - - /* - * This method is responsible for choosing the best media file to play according to the user's - * screen resolution and internet connection speed - */ - static getBestMediaFile = (mediaFilesAvailable) => { - // select the best media file based on internet bandwidth and screen size/resolution - const videojsVhs = localStorage.getItem('videojs-vhs'); - const bandwidth = videojsVhs ? JSON.parse(videojsVhs).bandwidth : undefined; - - let bestMediaFile = mediaFilesAvailable[0]; - - if (mediaFilesAvailable && bandwidth) { - const { height } = window.screen; - const { width } = window.screen; - - const result = mediaFilesAvailable - .sort((a, b) => ((Math.abs(a.bitrate - bandwidth) - Math.abs(b.bitrate - bandwidth)) - || (Math.abs(a.width - width) - Math.abs(b.width - width)) - || (Math.abs(a.height - height) - Math.abs(b.height - height)))); - - [bestMediaFile] = result; - } - - return bestMediaFile; - }; - - /* - * This method is responsible for choosing the best URl to redirect the user to when he clicks - * on the ad - */ - static getBestCtaUrl = (creative) => { - if ( - creative.videoClickThroughURLTemplate - && creative.videoClickThroughURLTemplate.url) { - return creative.videoClickThroughURLTemplate.url; - } - return false; - }; - - static getMidrolls = (adBreaks) => { - const midrolls = []; - if (adBreaks) { - return adBreaks - .filter((adBreak) => !['start', '0%', '00:00:00', 'end', '100%'].includes(adBreak.timeOffset)) - .reduce((prev, current) => ([ - ...prev, - { - timeOffset: current.timeOffset, - vastUrl: current.adSource.adTagURI?.uri, - vastData: current.adSource.vastAdData, - }, - ]), []); - } - return midrolls; - }; - - static getPreroll = (adBreaks) => { - if (adBreaks) { - return adBreaks.filter((adBreak) => ['start', '0%', '00:00:00'].includes(adBreak.timeOffset))[0]; - } - return false; - }; - - static getPostroll = (adBreaks) => { - if (adBreaks) { - return adBreaks.filter((adBreak) => ['end', '100%'].includes(adBreak.timeOffset))[0]; - } - return false; - }; - debug(msg, data = undefined) { if (!this.options.debug) { return; @@ -723,14 +631,78 @@ class Vast extends Plugin { this.removeEventsListeners(); super.dispose(); } -} + async handleVMAP(vmapUrl) { + try { + const vmap = await fetchVmapUrl(vmapUrl); + if (vmap.adBreaks && vmap.adBreaks.length > 0) { + this.addEventsListeners(); + // handle preroll + const preroll = getPreroll(vmap.adBreaks); + if (!preroll) { + this.disablePreroll(); + } else if (preroll.adSource?.adTagURI?.uri) { + // load vast preroll url + await this.handleVAST(preroll.adSource.adTagURI.uri); + // a preroll has been found, trigger adsready + this.player.trigger('adsready'); + } else if (preroll.adSource.vastAdData) { + this.parseInlineVastData(preroll.adSource?.vastAdData, 'preroll'); + } + // handle postroll + const postroll = getPostroll(vmap.adBreaks); + if (!postroll) { + this.disablePostroll(); + } else if (postroll.adSource?.adTagURI?.uri) { + this.postRollUrl = postroll.adSource.adTagURI.uri; + } else if (postroll.adSource?.vastAdData) { + this.parseInlineVastData(postroll.adSource?.vastAdData, 'postroll'); + } + this.watchForProgress = getMidrolls(vmap.adBreaks); + if (this.watchForProgress.length > 0) { + // listen on regular content for midroll handling + this.player.on('timeupdate', this.onProgress); + } + } + } catch (err) { + // could not fetch vmap + console.error(err); + } + } + + parseInlineVastData(vastAdData, adType) { + const xmlString = (new XMLSerializer()).serializeToString(vastAdData); + const vastXml = (new window.DOMParser()).parseFromString(xmlString, 'text/xml'); + const vastParser = new VASTParser(); + vastParser.parseVAST(vastXml) + .then((parsedVAST) => { + if (adType === 'postroll') { + // store for later use (in readyforpostroll event) + this.postRollData = parsedVAST.ads ?? []; + } else if (adType === 'preroll') { + this.adsArray = parsedVAST.ads ?? []; + this.player.trigger('adsready'); + } else if (adType === 'midroll') { + // store for later use (in readyforpostroll event) + this.adsArray = parsedVAST.ads ?? []; + this.readAd(); + } + }) + .catch((err) => { + console.log('error', err); + if (adType === 'postroll' || adType === 'midroll') { + this.disablePostroll(); + } else if (adType === 'preroll') { + // skip preroll, go ahaed to regular content + this.player.ads.skipLinearAdMode(); + } + }); + } +} Vast.prototype.playLinearAd = playLinearAd; Vast.prototype.playNonLinearAd = playNonLinearAd; Vast.prototype.playCompanionAd = playCompanionAd; Vast.prototype.addIcons = addIcons; -Vast.prototype.handleVMAP = handleVMAP; -Vast.prototype.parseInlineVastData = parseInlineVastData; export default Vast; diff --git a/src/lib/utils.js b/src/lib/utils.js index 9966d63..e04e0b5 100644 --- a/src/lib/utils.js +++ b/src/lib/utils.js @@ -27,3 +27,100 @@ export const convertTimeOffsetToSeconds = (timecode, duration = null) => { const [hours, minutes, seconds] = time.split(':'); return Number(`${parseInt(hours, 10) * 3600 + parseInt(minutes, 10) * 60 + parseInt(seconds, 10)}.${ms}`); }; + +/* + * This method is responsible for choosing the best media file to play according to the user's + * screen resolution and internet connection speed + */ +export const getBestMediaFile = (mediaFilesAvailable) => { + // select the best media file based on internet bandwidth and screen size/resolution + const videojsVhs = localStorage.getItem('videojs-vhs'); + const bandwidth = videojsVhs ? JSON.parse(videojsVhs).bandwidth : undefined; + + let bestMediaFile = mediaFilesAvailable[0]; + + if (mediaFilesAvailable && bandwidth) { + const { height } = window.screen; + const { width } = window.screen; + + const result = mediaFilesAvailable + .sort((a, b) => ((Math.abs(a.bitrate - bandwidth) - Math.abs(b.bitrate - bandwidth)) + || (Math.abs(a.width - width) - Math.abs(b.width - width)) + || (Math.abs(a.height - height) - Math.abs(b.height - height)))); + + [bestMediaFile] = result; + } + + return bestMediaFile; +}; + +export const applyNonLinearCommonDomStyle = (domElement) => { + domElement.style.cursor = 'pointer'; + domElement.style.left = '50%'; + domElement.style.position = 'absolute'; + domElement.style.transform = 'translateX(-50%)'; + domElement.style.bottom = '80px'; + domElement.style.display = 'block'; + domElement.style.zIndex = '2'; +}; + +export const getCloseButton = (clickCallback) => { + const closeButton = document.createElement('button'); + closeButton.addEventListener('click', clickCallback); + closeButton.style.width = '20px'; + closeButton.style.height = '20px'; + closeButton.style.position = 'absolute'; + closeButton.style.right = '5px'; + closeButton.style.top = '5px'; + closeButton.style.zIndex = '3'; + closeButton.style.background = '#CCC'; + closeButton.style.color = '#000'; + closeButton.style.fontSize = '12px'; + closeButton.style.cursor = 'pointer'; + closeButton.textContent = 'X'; + return closeButton; +}; + +/* +* This method is responsible for choosing the best URl to redirect the user to when he clicks +* on the ad +*/ +export const getBestCtaUrl = (creative) => { + if ( + creative.videoClickThroughURLTemplate + && creative.videoClickThroughURLTemplate.url) { + return creative.videoClickThroughURLTemplate.url; + } + return false; +}; + +export const getMidrolls = (adBreaks) => { + const midrolls = []; + if (adBreaks) { + return adBreaks + .filter((adBreak) => !['start', '0%', '00:00:00', 'end', '100%'].includes(adBreak.timeOffset)) + .reduce((prev, current) => ([ + ...prev, + { + timeOffset: current.timeOffset, + vastUrl: current.adSource.adTagURI?.uri, + vastData: current.adSource.vastAdData, + }, + ]), []); + } + return midrolls; +}; + +export const getPreroll = (adBreaks) => { + if (adBreaks) { + return adBreaks.filter((adBreak) => ['start', '0%', '00:00:00'].includes(adBreak.timeOffset))[0]; + } + return false; +}; + +export const getPostroll = (adBreaks) => { + if (adBreaks) { + return adBreaks.filter((adBreak) => ['end', '100%'].includes(adBreak.timeOffset))[0]; + } + return false; +}; diff --git a/src/modes/companions.js b/src/modes/companions.js index 9967e42..3a7a286 100644 --- a/src/modes/companions.js +++ b/src/modes/companions.js @@ -1,5 +1,6 @@ /* eslint-disable max-len */ -import Vast from '../index'; +import { applyNonLinearCommonDomStyle } from '../lib/utils'; + /* * This method is responsible for rendering a nonlinear ad */ @@ -16,7 +17,7 @@ export function playCompanionAd(creative) { ressourceContainer.height = variation.staticResources.height > 0 ? variation.staticResources.height : 100; ressourceContainer.style.maxWidth = variation.staticResources.expandedWidth; ressourceContainer.style.maxHeight = variation.staticResources.expandedHeight; - Vast.applyNonLinearCommonDomStyle(ressourceContainer); + applyNonLinearCommonDomStyle(ressourceContainer); const ressource = document.createElement('img'); this.domElements.push(ressourceContainer); @@ -44,7 +45,7 @@ export function playCompanionAd(creative) { ressourceContainer.height = variation.htmlResources.height; ressourceContainer.style.maxWidth = variation.htmlResources.expandedWidth; ressourceContainer.style.maxHeight = variation.htmlResources.expandedHeight; - Vast.applyNonLinearCommonDomStyle(ressourceContainer); + applyNonLinearCommonDomStyle(ressourceContainer); ressourceContainer.addEventListener('click', () => { window.open(variation.companionClickThroughURLTemplate, '_blank'); this.companionVastTracker.click(null, this.macros); @@ -68,7 +69,7 @@ export function playCompanionAd(creative) { ressourceContainer.height = variation.iframeResources.height; ressourceContainer.style.maxWidth = variation.iframeResources.expandedWidth; ressourceContainer.style.maxHeight = variation.iframeResources.expandedHeight; - Vast.applyNonLinearCommonDomStyle(ressourceContainer); + applyNonLinearCommonDomStyle(ressourceContainer); ressourceContainer.addEventListener('click', () => { window.open(variation.companionClickThroughURLTemplate, '_blank'); this.companionVastTracker.click(null, this.macros); diff --git a/src/modes/index.js b/src/modes/index.js deleted file mode 100644 index a1e920b..0000000 --- a/src/modes/index.js +++ /dev/null @@ -1,3 +0,0 @@ -export * from './linear'; -export * from './nonlinear'; -export * from './companions'; diff --git a/src/modes/linear.js b/src/modes/linear.js index ba1c32a..be9f875 100644 --- a/src/modes/linear.js +++ b/src/modes/linear.js @@ -1,11 +1,11 @@ -import Vast from '../index'; +import { getBestMediaFile } from '../lib/utils'; /* * This method is responsible for rendering a linear ad */ export function playLinearAd(creative) { this.debug('playLinearAd', creative); // Retrieve the media file from the VAST manifest - const mediaFile = Vast.getBestMediaFile(creative.mediaFiles); + const mediaFile = getBestMediaFile(creative.mediaFiles); // Start ad mode if (!this.player.ads.inAdBreak()) { diff --git a/src/modes/nonlinear.js b/src/modes/nonlinear.js index c9671a3..0cfb4d4 100644 --- a/src/modes/nonlinear.js +++ b/src/modes/nonlinear.js @@ -1,4 +1,5 @@ -import Vast from '../index'; +import { applyNonLinearCommonDomStyle, getCloseButton } from '../lib/utils'; + /* * This method is responsible for rendering a nonlinear ad */ @@ -10,7 +11,7 @@ export function playNonLinearAd(creative) { if (variation.staticResource) { const ressourceContainer = document.createElement('div'); this.domElements.push(ressourceContainer); - Vast.applyNonLinearCommonDomStyle(ressourceContainer); + applyNonLinearCommonDomStyle(ressourceContainer); const ressource = document.createElement('img'); ressource.addEventListener('click', () => { @@ -22,7 +23,7 @@ export function playNonLinearAd(creative) { ressource.src = variation.staticResource; // add close button - const closeButton = Vast.getCloseButton(() => ressourceContainer.remove()); + const closeButton = getCloseButton(() => ressourceContainer.remove()); closeButton.style.display = variation.minSuggestedDuration ? 'none' : 'block'; if (variation.minSuggestedDuration) { @@ -43,7 +44,7 @@ export function playNonLinearAd(creative) { if (variation.htmlResource) { const ressourceContainer = document.createElement('div'); this.domElements.push(ressourceContainer); - Vast.applyNonLinearCommonDomStyle(ressourceContainer); + applyNonLinearCommonDomStyle(ressourceContainer); ressourceContainer.addEventListener('click', () => { window.open(variation.nonlinearClickThroughURLTemplate, '_blank'); this.nonLinearVastTracker.click(null, this.macros); @@ -69,7 +70,7 @@ export function playNonLinearAd(creative) { if (variation.iframeResource) { const ressourceContainer = document.createElement('iframe'); this.domElements.push(ressourceContainer); - Vast.applyNonLinearCommonDomStyle(ressourceContainer); + applyNonLinearCommonDomStyle(ressourceContainer); ressourceContainer.addEventListener('click', () => { window.open(variation.nonlinearClickThroughURLTemplate, '_blank'); this.nonLinearVastTracker.click(null, this.macros);