'use strict'; /** * VAST Ads Plugin * */ // Feature configuration Object.assign(mejs.MepDefaults, { /** * URL to vast data (http://minotovideo.com/sites/minotovideo.com/files/upload/eday_vast_tag.xml) * @type {String} */ vastAdTagUrl: '', /** * Whether Ads is VAST or VPAID * @type {String} */ vastAdsType: 'vast' }); Object.assign(MediaElementPlayer.prototype, { /** * @type {Boolean} */ vastAdTagIsLoading: false, /** * @type {Boolean} */ vastAdTagIsLoaded: false, /** * @type {Boolean} */ vastStartedPlaying: false, /** * @type {Array} */ vastAdTags: [], /** * Feature constructor. * * Always has to be prefixed with `build` and the name that will be used in MepDefaults.features list * @param {MediaElementPlayer} player * @param {HTMLElement} controls * @param {HTMLElement} layers * @param {HTMLElement} media */ buildvast (player, controls, layers, media) { const t = this; if (!t.isVideo) { return; } // begin loading if (t.options.vastAdTagUrl !== '') { t.vastLoadAdTagInfo(); } // make sure the preroll ad system is ready (it will ensure it can't be called twice) t.buildads(player, controls, layers, media); t.vastSetupEvents(); }, vastSetupEvents () { const t = this; let firstQuartExecuted = false, secondQuartExecuted = false, thirdQuartExecuted = false ; // LOAD: preroll t.container.addEventListener('mejsprerollinitialized', () => { if (t.vastAdTags.length > 0) { const adTag = t.vastAdTags[0]; if (adTag.trackingEvents.initialization) { for (let i = 0, total = adTag.trackingEvents.initialization.length; i < total; i++) { t.adsLoadUrl(adTag.trackingEvents.initialization[i]); } } } }); // START: preroll t.container.addEventListener('mejsprerollstarted', () => { if (t.vastAdTags.length > 0) { const adTag = t.vastAdTags[0]; // always fire this event if (adTag.trackingEvents.start) { for (let i = 0, total = adTag.trackingEvents.start.length; i < total; i++) { t.adsLoadUrl(adTag.trackingEvents.start[i]); } } // only do impressions once if (!adTag.shown && adTag.impressions.length > 0) { for (let i = 0, total = adTag.impressions.length; i < total; i++) { t.adsLoadUrl(adTag.impressions[i]); } } adTag.shown = true; } }); // VOLUMECHANGE: preroll t.container.addEventListener('mejsprerollvolumechanged', () => { if (t.vastAdTags.length > 0 && t.options.indexPreroll < t.vastAdTags.length) { const adTag = t.vastAdTags[t.options.indexPreroll]; if (adTag.trackingEvents.mute && !t.media.volume) { for (let i = 0, total = adTag.trackingEvents.mute.length; i < total; i++) { t.adsLoadUrl(adTag.trackingEvents.mute[i]); } } if (adTag.trackingEvents.unmute && t.media.volume) { for (let i = 0, total = adTag.trackingEvents.unmute.length; i < total; i++) { t.adsLoadUrl(adTag.trackingEvents.unmute[i]); } } } }); // UPDATE: preroll t.container.addEventListener('mejsprerolltimeupdate', (e) => { if (t.vastAdTags.length > 0 && t.options.indexPreroll < t.vastAdTags.length) { const duration = e.detail.duration, current = e.detail.currentTime, percentage = Math.min(1, Math.max(0, current / duration)) * 100, adTag = t.vastAdTags[t.options.indexPreroll], isFirsQuart = percentage >= 25 && percentage < 50, isMidPoint = percentage >= 50 && percentage < 75, isThirdQuart = percentage >= 75 && percentage < 100 ; // Check which track is going to be fired if (adTag.trackingEvents.firstQuartile && !firstQuartExecuted && isFirsQuart) { for (let i = 0, total = adTag.trackingEvents.firstQuartile.length; i < total; i++) { t.adsLoadUrl(adTag.trackingEvents.firstQuartile[i]); } firstQuartExecuted = true; } else if (adTag.trackingEvents.midpoint && !secondQuartExecuted && isMidPoint) { for (let i = 0, total = adTag.trackingEvents.midpoint.length; i < total; i++) { t.adsLoadUrl(adTag.trackingEvents.midpoint[i]); } secondQuartExecuted = true; } else if (adTag.trackingEvents.thirdQuartile && !thirdQuartExecuted && isThirdQuart) { for (let i = 0, total = adTag.trackingEvents.thirdQuartile.length; i < total; i++) { t.adsLoadUrl(adTag.trackingEvents.thirdQuartile[i]); } thirdQuartExecuted = true; } } }); // END: preroll t.container.addEventListener('mejsprerollended', () => { const adTag = t.vastAdTags[t.options.indexPreroll]; if (t.vastAdTags.length > 0 && t.options.indexPreroll < t.vastAdTags.length && adTag.trackingEvents.complete) { for (let i = 0, total = adTag.trackingEvents.complete.length; i < total; i++) { t.adsLoadUrl(adTag.trackingEvents.complete[i]); } } firstQuartExecuted = false; secondQuartExecuted = false; thirdQuartExecuted = false; }); // ADCLICKED: preroll t.container.addEventListener('mejsprerolladsclicked', () => { const adTag = t.vastAdTags[t.options.indexPreroll]; if (t.vastAdTags.length > 0 && t.options.indexPreroll < t.vastAdTags.length && adTag.clickThrough && adTag.clickTracking) { t.adsLoadUrl(adTag.clickTracking); } }); // ADSKIPPED: preroll t.container.addEventListener('mejsprerollskipclicked', () => { const adTag = t.vastAdTags[t.options.indexPreroll]; if (t.vastAdTags.length > 0 && t.options.indexPreroll < t.vastAdTags.length && adTag.trackingEvents.skip) { for (let i = 0, total = adTag.trackingEvents.skip.length; i < total; i++) { t.adsLoadUrl(adTag.trackingEvents.skip[i]); } } }); }, /** * * @param {String} url */ vastSetAdTagUrl (url) { const t = this; // set and reset t.options.vastAdTagUrl = url; t.options.indexPreroll = 0; t.vastAdTagIsLoaded = false; t.vastAdTags = []; }, /** * */ vastLoadAdTagInfo () { const t = this; // set this to stop playback t.adsDataIsLoading = true; t.vastAdTagIsLoading = true; // try straight load first t.loadAdTagInfoDirect(); }, /** * */ loadAdTagInfoDirect () { const t = this; mejs.Utils.ajax(t.options.vastAdTagUrl, 'xml', (data) => { if (t.options.vastAdsType === 'vpaid') { t.vpaidParseVpaidData(data); } else { t.vastParseVastData(data); } }, (err) => { console.error('vast3:direct:error', err); // fallback to Yahoo proxy t.loadAdTagInfoProxy(); }); }, /** * */ loadAdTagInfoProxy () { const t = this, protocol = location.protocol, query = `select * from xml where url="${encodeURI(t.options.vastAdTagUrl)}"`, yahooUrl = `http${(/^https/.test(protocol) ? 's' : '')}://query.yahooapis.com/v1/public/yql?format=xml&q=${query}` ; mejs.Utils.ajax(yahooUrl, 'xml', (data) => { if (t.options.vastAdsType === 'vpaid') { t.vpaidParseVpaidData(data); } else { t.vastParseVastData(data); } }, (err) => { console.error('vast:proxy:yahoo:error', err); }); }, /** * Parse a VAST XML source and build adTags entities. * * This is compliant with VPAID 3.0 * @param {String} data */ vastParseVastData (data) { const t = this, ads = data.getElementsByTagName('Ad') ; if (!ads.length) { return; } // clear out data t.vastAdTags = []; t.options.indexPreroll = 0; for (let i = 0, total = ads.length; i < total; i++) { const adNode = ads[i], title = adNode.getElementsByTagName('AdTitle').length ? adNode.getElementsByTagName('AdTitle')[0].textContent.trim() : '', description = adNode.getElementsByTagName('Description').length ? adNode.getElementsByTagName('Description')[0].textContent.trim() : '', clickLink = adNode.getElementsByTagName('ClickThrough').length ? adNode.getElementsByTagName('ClickThrough')[0].textContent.trim() : '', clickTrack = adNode.getElementsByTagName('ClickTracking').length ? adNode.getElementsByTagName('ClickTracking')[0].textContent.trim() : '', adTag = { id: adNode.getAttribute('id'), title: title, description: description, impressions: [], clickThrough: clickLink, clickTracking: clickTrack, mediaFiles: [], trackingEvents: {}, // internal tracking if it's been used shown: false }, impressions = adNode.getElementsByTagName('Impression'), mediaFiles = adNode.getElementsByTagName('MediaFile'), trackFiles = adNode.getElementsByTagName('Tracking') ; t.vastAdTags.push(adTag); for (let j = 0, impressionsTotal = impressions.length; j < impressionsTotal; j++) { adTag.impressions.push(impressions[j].textContent.trim()); } for (let j = 0, tracksTotal = trackFiles.length; j < tracksTotal; j++) { const trackingEvent = trackFiles[j], event = trackingEvent.getAttribute('event'); if (adTag.trackingEvents[event] === undefined) { adTag.trackingEvents[event] = []; } adTag.trackingEvents[event].push(trackingEvent.textContent.trim()); } for (let j = 0, mediaFilesTotal = mediaFiles.length; j < mediaFilesTotal; j++) { const mediaFile = mediaFiles[j], type = mediaFile.getAttribute('type') ; if (t.media.canPlayType(type) !== '' || /(no|false)/i.test(t.media.canPlayType(type))) { // Execute JS files if found if (type === 'application/javascript') { const script = document.createElement('script'), firstScriptTag = document.getElementsByTagName('script')[0] ; script.src = mediaFile.textContent.trim(); firstScriptTag.parentNode.insertBefore(script, firstScriptTag); } // Avoid Flash else if (type !== 'application/x-shockwave-flash') { adTag.mediaFiles.push({ id: mediaFile.getAttribute('id'), delivery: mediaFile.getAttribute('delivery'), type: type, bitrate: mediaFile.getAttribute('bitrate'), width: mediaFile.getAttribute('width'), height: mediaFile.getAttribute('height'), url: mediaFile.textContent.trim() }); } } } } // DONE t.vastLoaded(); }, /** * Parse a VPAID XML source and build adTags entities. * * This is compliant with VPAID 2.0 * @param {String} data */ vpaidParseVpaidData (data) { const t = this, ads = data.getElementsByTagName('AdParameters') ; // clear out data t.vpaidAdTags = []; t.options.indexPreroll = 0; if (typeof ads[0] === 'undefined') { return; } const adData = JSON.parse(ads[0].textContent.trim()), duration = data.getElementsByTagName('Duration'), adTag = { id: adData.ad_id.trim(), title: adData.title.trim(), clickThrough: adData.page_url, impressions: [], mediaFiles: [], trackingEvents: {}, // internal tracking if it's been used shown: false } ; if (typeof adData.media.tracking.beacon !== 'undefined') { const trackingPoints = ['initialization', 'start', 'firstQuartile', 'midpoint', 'thirdQuartile', 'complete']; for (let i = 0, total = adData.media.tracking.beacon.length; i < total; i++) { const trackingEvent = adData.media.tracking.beacon[i]; if (~trackingPoints.indexOf(trackingEvent.type)) { if (adTag.trackingEvents[trackingEvent.type] === undefined) { adTag.trackingEvents[trackingEvent.type] = []; } adTag.trackingEvents[trackingEvent.type].push(trackingEvent.beacon_url.trim()); } else if (trackingEvent.type === 'impression') { adTag.impressions.push(trackingEvent.beacon_url.trim()); } } } for (const property in adData.media.video) { if (adData.media.video.hasOwnProperty(property)) { const mediaFile = adData.media.video[property], type = mediaFile.mime_type.trim() ; if (t.media.canPlayType(type) !== '' || /(no|false)/i.test(t.media.canPlayType(type))) { adTag.mediaFiles.push({ id: mediaFile.media_id, format: mediaFile.format, type: type, transcoding: mediaFile.transcoding, width: mediaFile.width, height: mediaFile.height, duration: duration, url: mediaFile.media_url }); } } } t.vastAdTags.push(adTag); // DONE t.vastLoaded(); }, /** * */ vastLoaded () { const t = this; t.vastAdTagIsLoaded = true; t.vastAdTagIsLoading = false; t.adsDataIsLoading = false; t.vastStartPreroll(); }, /** * */ vastStartPreroll () { const t = this; // if we have a media URL, then send it up to the ads plugin as a preroll // load up the vast ads to be played before the selected media. // Note: multiple preroll ads are supported. let i = 0; while (i < t.vastAdTags.length) { if (typeof t.vastAdTags[i].mediaFiles !== 'undefined' && t.vastAdTags[i].mediaFiles.length) { t.options.adsPrerollMediaUrl[i] = t.vastAdTags[i].mediaFiles[0].url; } if (typeof t.vastAdTags[i].clickThrough !== 'undefined') { t.options.adsPrerollAdUrl[i] = t.vastAdTags[i].clickThrough; } i++; } t.adsStartPreroll(); } });