1123 lines
43 KiB
JavaScript
1123 lines
43 KiB
JavaScript
// @ts-check
|
|
|
|
// VAST support module
|
|
|
|
/* Type declarations */
|
|
|
|
/**
|
|
* @typedef {Object} RawAdTree
|
|
* @property {Array<RawAdTree>} children
|
|
* @property {XMLDocument} data
|
|
* @property {'inLine'|'wrapper'} tagType
|
|
* @property {boolean|undefined} fallbackOnNoAd
|
|
* @property {Array<XMLDocument> | undefined} wrappers
|
|
*/
|
|
|
|
/**
|
|
* @typedef {Object} RawAd
|
|
* @property {XMLDocument} data
|
|
* @property {Array<XMLDocument>} wrappers
|
|
* @property {'inLine' | 'wrapper'} tagType
|
|
*/
|
|
|
|
/**
|
|
* @typedef {Object & RawAd} Ad
|
|
* @property {Array<string>} clicktracking
|
|
* @property {string} errorUrl
|
|
* @property {Array<string>} impressions
|
|
* @property {Array<string>} viewImpression
|
|
* @property {Array<any>} stopTracking
|
|
* @property {Array<any>} tracking
|
|
* @property {number|null} sequence
|
|
* @property {number} duration
|
|
* @property {boolean} played
|
|
*/
|
|
|
|
/**
|
|
*
|
|
* @param {import("../fluidplayer.js").FluidPlayer} playerInstance
|
|
* @param {unknown} options
|
|
*/
|
|
function vast(playerInstance, options) {
|
|
/**
|
|
* Gets CTA parameters from VAST and sets them on tempOptions
|
|
*
|
|
* Fallbacks to any value that is filled on the TitleCTA extension, but needs at least an url and a text
|
|
*
|
|
* @param {HTMLElement} titleCtaElement
|
|
*
|
|
* @param {any} tmpOptions
|
|
*/
|
|
playerInstance.setCTAFromVast = (titleCtaElement, tmpOptions) => {
|
|
if (playerInstance.displayOptions.vastOptions.adCTATextVast && titleCtaElement) {
|
|
const mobileText = playerInstance.extractNodeDataByTagName(titleCtaElement, 'MobileText');
|
|
const desktopText = playerInstance.extractNodeDataByTagName(titleCtaElement, 'PCText');
|
|
const link =
|
|
playerInstance.extractNodeDataByTagName(titleCtaElement, 'DisplayUrl') ||
|
|
playerInstance.extractNodeDataByTagName(titleCtaElement, 'Link');
|
|
const tracking = playerInstance.extractNodeDataByTagName(titleCtaElement, 'Tracking');
|
|
const isMobile = window.matchMedia('(max-width: 768px)').matches;
|
|
|
|
if ((desktopText || mobileText) && tracking) {
|
|
tmpOptions.titleCTA = {
|
|
text: isMobile ?
|
|
mobileText || desktopText :
|
|
desktopText || mobileText,
|
|
link: link || null,
|
|
tracking,
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
playerInstance.getClickThroughUrlFromLinear = (linear) => {
|
|
const videoClicks = linear.getElementsByTagName('VideoClicks');
|
|
|
|
if (videoClicks.length) { //There should be exactly 1 node
|
|
const clickThroughs = videoClicks[0].getElementsByTagName('ClickThrough');
|
|
|
|
if (clickThroughs.length) {
|
|
return playerInstance.extractNodeData(clickThroughs[0]);
|
|
}
|
|
}
|
|
|
|
return false;
|
|
};
|
|
|
|
playerInstance.getVastAdTagUriFromWrapper = (xmlResponse) => {
|
|
const wrapper = xmlResponse.getElementsByTagName('Wrapper');
|
|
|
|
if (typeof wrapper !== 'undefined' && wrapper.length) {
|
|
const vastAdTagURI = wrapper[0].getElementsByTagName('VASTAdTagURI');
|
|
|
|
if (vastAdTagURI.length) {
|
|
return playerInstance.extractNodeData(vastAdTagURI[0]);
|
|
}
|
|
}
|
|
|
|
return false;
|
|
};
|
|
|
|
playerInstance.hasInLine = (xmlResponse) => {
|
|
const inLine = xmlResponse.getElementsByTagName('InLine');
|
|
return ((typeof inLine !== 'undefined') && inLine.length);
|
|
};
|
|
|
|
playerInstance.hasVastAdTagUri = (xmlResponse) => {
|
|
const vastAdTagURI = xmlResponse.getElementsByTagName('VASTAdTagURI');
|
|
return ((typeof vastAdTagURI !== 'undefined') && vastAdTagURI.length);
|
|
};
|
|
|
|
playerInstance.getClickThroughUrlFromNonLinear = (nonLinear) => {
|
|
let result = '';
|
|
const nonLinears = nonLinear.getElementsByTagName('NonLinear');
|
|
|
|
if (nonLinears.length) {//There should be exactly 1 node
|
|
const nonLinearClickThrough = nonLinear.getElementsByTagName('NonLinearClickThrough');
|
|
if (nonLinearClickThrough.length) {
|
|
result = playerInstance.extractNodeData(nonLinearClickThrough[0]);
|
|
}
|
|
}
|
|
|
|
return result;
|
|
};
|
|
|
|
playerInstance.getTrackingFromLinear = (linear) => {
|
|
const trackingEvents = linear.getElementsByTagName('TrackingEvents');
|
|
|
|
if (trackingEvents.length) {//There should be no more than one node
|
|
return trackingEvents[0].getElementsByTagName('Tracking');
|
|
}
|
|
|
|
return [];
|
|
};
|
|
|
|
playerInstance.getDurationFromLinear = (linear) => {
|
|
const duration = linear.getElementsByTagName('Duration');
|
|
|
|
if (duration.length && (typeof duration[0].childNodes[0] !== 'undefined')) {
|
|
const nodeDuration = playerInstance.extractNodeData(duration[0]);
|
|
return playerInstance.convertTimeStringToSeconds(nodeDuration);
|
|
}
|
|
|
|
return false;
|
|
};
|
|
|
|
playerInstance.getDurationFromNonLinear = (tag) => {
|
|
let result = 0;
|
|
const nonLinear = tag.getElementsByTagName('NonLinear');
|
|
if (nonLinear.length && (typeof nonLinear[0].getAttribute('minSuggestedDuration') !== 'undefined')) {
|
|
result = playerInstance.convertTimeStringToSeconds(nonLinear[0].getAttribute('minSuggestedDuration'));
|
|
}
|
|
return result;
|
|
};
|
|
|
|
playerInstance.getDimensionFromNonLinear = (tag) => {
|
|
const result = { 'width': null, 'height': null };
|
|
const nonLinear = tag.getElementsByTagName('NonLinear');
|
|
|
|
if (nonLinear.length) {
|
|
if (typeof nonLinear[0].getAttribute('width') !== 'undefined') {
|
|
result.width = nonLinear[0].getAttribute('width');
|
|
}
|
|
if (typeof nonLinear[0].getAttribute('height') !== 'undefined') {
|
|
result.height = nonLinear[0].getAttribute('height');
|
|
}
|
|
}
|
|
|
|
return result;
|
|
};
|
|
|
|
playerInstance.getCreativeTypeFromStaticResources = (tag) => {
|
|
let result = '';
|
|
const nonLinears = tag.getElementsByTagName('NonLinear');
|
|
|
|
if (nonLinears.length && (typeof nonLinears[0].childNodes[0] !== 'undefined')) {//There should be exactly 1 StaticResource node
|
|
result = nonLinears[0].getElementsByTagName('StaticResource')[0].getAttribute('creativeType');
|
|
}
|
|
|
|
return result.toLowerCase();
|
|
};
|
|
|
|
playerInstance.getMediaFilesFromLinear = (linear) => {
|
|
const mediaFiles = linear.getElementsByTagName('MediaFiles');
|
|
|
|
if (mediaFiles.length) {//There should be exactly 1 MediaFiles node
|
|
return mediaFiles[0].getElementsByTagName('MediaFile');
|
|
}
|
|
|
|
return [];
|
|
};
|
|
|
|
playerInstance.getStaticResourcesFromNonLinear = (linear) => {
|
|
let result = [];
|
|
const nonLinears = linear.getElementsByTagName('NonLinear');
|
|
|
|
if (nonLinears.length) {//There should be exactly 1 StaticResource node
|
|
result = nonLinears[0].getElementsByTagName('StaticResource');
|
|
}
|
|
|
|
return result;
|
|
};
|
|
|
|
/**
|
|
* Gets the first element found by tag name, and returns the element data
|
|
*
|
|
* @param {HTMLElement} parentNode
|
|
*
|
|
* @param {string} tagName
|
|
*
|
|
* @returns {string|null}
|
|
*/
|
|
playerInstance.extractNodeDataByTagName = (parentNode, tagName) => {
|
|
const element = parentNode.getElementsByTagName(tagName);
|
|
|
|
if (element && element.length) {
|
|
return playerInstance.extractNodeData(element[0]);
|
|
} else {
|
|
return null;
|
|
}
|
|
};
|
|
|
|
playerInstance.extractNodeData = (parentNode) => {
|
|
let contentAsString = "";
|
|
for (let n = 0; n < parentNode.childNodes.length; n++) {
|
|
const child = parentNode.childNodes[n];
|
|
if (child.nodeType === 8 || (child.nodeType === 3 && /^\s*$/.test(child.nodeValue))) {
|
|
// Comments or text with no content
|
|
} else {
|
|
contentAsString += child.nodeValue;
|
|
}
|
|
}
|
|
return contentAsString.replace(/(^\s+|\s+$)/g, '');
|
|
};
|
|
|
|
playerInstance.getAdParametersFromLinear = (linear) => {
|
|
const adParameters = linear.getElementsByTagName('AdParameters');
|
|
let adParametersData = null;
|
|
|
|
if (adParameters.length) {
|
|
adParametersData = playerInstance.extractNodeData(adParameters[0]);
|
|
}
|
|
|
|
return adParametersData;
|
|
};
|
|
|
|
playerInstance.getMediaFileListFromLinear = (linear) => {
|
|
const mediaFileList = [];
|
|
const mediaFiles = playerInstance.getMediaFilesFromLinear(linear);
|
|
|
|
if (!mediaFiles.length) {
|
|
return mediaFileList;
|
|
}
|
|
|
|
for (let n = 0; n < mediaFiles.length; n++) {
|
|
let mediaType = mediaFiles[n].getAttribute('mediaType');
|
|
|
|
if (!mediaType) {
|
|
// if there is no mediaType attribute then the video is 2D
|
|
mediaType = '2D';
|
|
}
|
|
|
|
// get all the attributes of media file
|
|
mediaFileList.push({
|
|
'src': playerInstance.extractNodeData(mediaFiles[n]),
|
|
'type': mediaFiles[n].getAttribute('type'),
|
|
'apiFramework': mediaFiles[n].getAttribute('apiFramework'),
|
|
'codec': mediaFiles[n].getAttribute('codec'),
|
|
'id': mediaFiles[n].getAttribute('codec'),
|
|
'fileSize': mediaFiles[n].getAttribute('fileSize'),
|
|
'delivery': mediaFiles[n].getAttribute('delivery'),
|
|
'width': mediaFiles[n].getAttribute('width'),
|
|
'height': mediaFiles[n].getAttribute('height'),
|
|
'mediaType': mediaType.toLowerCase()
|
|
});
|
|
|
|
}
|
|
|
|
return mediaFileList;
|
|
};
|
|
|
|
playerInstance.getIconClickThroughFromLinear = (linear) => {
|
|
const iconClickThrough = linear.getElementsByTagName('IconClickThrough');
|
|
|
|
if (iconClickThrough.length) {
|
|
return playerInstance.extractNodeData(iconClickThrough[0]);
|
|
}
|
|
|
|
return '';
|
|
};
|
|
|
|
playerInstance.getStaticResourceFromNonLinear = (linear) => {
|
|
let fallbackStaticResource;
|
|
const staticResources = playerInstance.getStaticResourcesFromNonLinear(linear);
|
|
|
|
for (let i = 0; i < staticResources.length; i++) {
|
|
if (!staticResources[i].getAttribute('type')) {
|
|
fallbackStaticResource = playerInstance.extractNodeData(staticResources[i]);
|
|
}
|
|
|
|
if (staticResources[i].getAttribute('type') === playerInstance.displayOptions.staticResource) {
|
|
return playerInstance.extractNodeData(staticResources[i]);
|
|
}
|
|
}
|
|
|
|
return fallbackStaticResource;
|
|
};
|
|
|
|
playerInstance.registerTrackingEvents = (creativeLinear, tmpOptions) => {
|
|
const trackingEvents = playerInstance.getTrackingFromLinear(creativeLinear);
|
|
let eventType = '';
|
|
let oneEventOffset = 0;
|
|
|
|
for (let i = 0; i < trackingEvents.length; i++) {
|
|
eventType = trackingEvents[i].getAttribute('event');
|
|
|
|
switch (eventType) {
|
|
case 'start':
|
|
case 'firstQuartile':
|
|
case 'midpoint':
|
|
case 'thirdQuartile':
|
|
case 'complete':
|
|
if (typeof tmpOptions.tracking[eventType] === 'undefined') {
|
|
tmpOptions.tracking[eventType] = [];
|
|
}
|
|
|
|
if (typeof tmpOptions.stopTracking[eventType] === 'undefined') {
|
|
tmpOptions.stopTracking[eventType] = [];
|
|
}
|
|
tmpOptions.tracking[eventType].push(trackingEvents[i].textContent.trim());
|
|
tmpOptions.stopTracking[eventType] = false;
|
|
|
|
break;
|
|
|
|
case 'progress':
|
|
if (typeof tmpOptions.tracking[eventType] === 'undefined') {
|
|
tmpOptions.tracking[eventType] = [];
|
|
}
|
|
|
|
oneEventOffset = playerInstance.convertTimeStringToSeconds(trackingEvents[i].getAttribute('offset'));
|
|
|
|
if (typeof tmpOptions.tracking[eventType][oneEventOffset] === 'undefined') {
|
|
tmpOptions.tracking[eventType][oneEventOffset] = {
|
|
elements: [],
|
|
stopTracking: false
|
|
};
|
|
}
|
|
|
|
tmpOptions.tracking[eventType][oneEventOffset].elements.push(trackingEvents[i].textContent.trim());
|
|
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
};
|
|
|
|
playerInstance.registerClickTracking = (clickTrackingTag, tmpOptions) => {
|
|
if (!clickTrackingTag || !clickTrackingTag.length) {
|
|
return;
|
|
}
|
|
|
|
for (let i = 0; i < clickTrackingTag.length; i++) {
|
|
if (clickTrackingTag[i] === '') {
|
|
continue;
|
|
}
|
|
|
|
tmpOptions.clicktracking.push(clickTrackingTag[i]);
|
|
}
|
|
|
|
};
|
|
|
|
playerInstance.registerViewableImpressionEvents = (viewableImpressionTags, tmpOptions) => {
|
|
if (!viewableImpressionTags.length) {
|
|
return;
|
|
}
|
|
|
|
for (let i = 0; i < viewableImpressionTags.length; i++) {
|
|
const viewableImpressionEvent = playerInstance.extractNodeData(viewableImpressionTags[i]);
|
|
tmpOptions.viewImpression.push(viewableImpressionEvent);
|
|
}
|
|
};
|
|
|
|
playerInstance.registerImpressionEvents = (impressionTags, tmpOptions) => {
|
|
if (!impressionTags.length) {
|
|
return;
|
|
}
|
|
|
|
for (let i = 0; i < impressionTags.length; i++) {
|
|
const impressionEvent = playerInstance.extractNodeData(impressionTags[i]);
|
|
tmpOptions.impression.push(impressionEvent);
|
|
}
|
|
};
|
|
|
|
playerInstance.registerErrorEvents = (errorTags, tmpOptions) => {
|
|
if ((typeof errorTags !== 'undefined') &&
|
|
(errorTags !== null) &&
|
|
(errorTags.length === 1) && //Only 1 Error tag is expected
|
|
(errorTags[0].childNodes.length === 1)) {
|
|
tmpOptions.errorUrl = errorTags[0].childNodes[0].nodeValue;
|
|
}
|
|
};
|
|
|
|
playerInstance.announceError = (code) => {
|
|
if (typeof playerInstance.vastOptions.errorUrl === 'undefined' || !playerInstance.vastOptions.errorUrl) {
|
|
return;
|
|
}
|
|
|
|
const parsedCode = typeof code !== 'undefined' ? parseInt(code) : 900;
|
|
const errorUrl = playerInstance.vastOptions.errorUrl.replace('[ERRORCODE]', parsedCode);
|
|
|
|
//Send the error request
|
|
playerInstance.callUris([errorUrl]);
|
|
};
|
|
|
|
playerInstance.getClickTrackingEvents = (linear) => {
|
|
const result = [];
|
|
|
|
const videoClicks = linear.getElementsByTagName('VideoClicks');
|
|
|
|
//There should be exactly 1 node
|
|
if (!videoClicks.length) {
|
|
return;
|
|
}
|
|
|
|
const clickTracking = videoClicks[0].getElementsByTagName('ClickTracking');
|
|
|
|
if (!clickTracking.length) {
|
|
return;
|
|
}
|
|
|
|
for (let i = 0; i < clickTracking.length; i++) {
|
|
const clickTrackingEvent = playerInstance.extractNodeData(clickTracking[i]);
|
|
result.push(clickTrackingEvent);
|
|
}
|
|
|
|
return result;
|
|
};
|
|
|
|
playerInstance.getNonLinearClickTrackingEvents = (nonLinear) => {
|
|
const result = [];
|
|
const nonLinears = nonLinear.getElementsByTagName('NonLinear');
|
|
|
|
if (!nonLinears.length) {
|
|
return;
|
|
}
|
|
|
|
const clickTracking = nonLinear.getElementsByTagName('NonLinearClickTracking');
|
|
|
|
if (!clickTracking.length) {
|
|
return;
|
|
}
|
|
|
|
for (let i = 0; i < clickTracking.length; i++) {
|
|
const NonLinearClickTracking = playerInstance.extractNodeData(clickTracking[i]);
|
|
result.push(NonLinearClickTracking);
|
|
}
|
|
|
|
return result;
|
|
};
|
|
|
|
// TODO: ???
|
|
playerInstance.callUris = (uris) => {
|
|
for (let i = 0; i < uris.length; i++) {
|
|
new Image().src = uris[i];
|
|
}
|
|
};
|
|
|
|
playerInstance.recalculateAdDimensions = () => {
|
|
const videoPlayer = playerInstance.domRef.player;
|
|
const divClickThrough = playerInstance.domRef.wrapper.querySelector('.vast_clickthrough_layer');
|
|
|
|
if (divClickThrough) {
|
|
divClickThrough.style.width = videoPlayer.offsetWidth + 'px';
|
|
divClickThrough.style.height = videoPlayer.offsetHeight + 'px';
|
|
}
|
|
|
|
const requestFullscreenFunctionNames = playerInstance.checkFullscreenSupport();
|
|
const fullscreenButton = playerInstance.domRef.wrapper.querySelector('.fluid_control_fullscreen');
|
|
const menuOptionFullscreen = playerInstance.domRef.wrapper.querySelector('.context_option_fullscreen');
|
|
|
|
if (requestFullscreenFunctionNames) {
|
|
// this will go other way around because we already exited full screen
|
|
if (document[requestFullscreenFunctionNames.isFullscreen] === null) {
|
|
// Exit fullscreen
|
|
playerInstance.fullscreenOff(fullscreenButton, menuOptionFullscreen);
|
|
} else {
|
|
// Go fullscreen
|
|
playerInstance.fullscreenOn(fullscreenButton, menuOptionFullscreen);
|
|
}
|
|
} else {
|
|
// TODO: I am fairly certain this fallback does not work...
|
|
//The browser does not support the Fullscreen API, so a pseudo-fullscreen implementation is used
|
|
const fullscreenTag = playerInstance.domRef.wrapper;
|
|
|
|
if (fullscreenTag.className.search(/\bpseudo_fullscreen\b/g) !== -1) {
|
|
fullscreenTag.className += ' pseudo_fullscreen';
|
|
playerInstance.fullscreenOn(fullscreenButton, menuOptionFullscreen);
|
|
} else {
|
|
fullscreenTag.className = fullscreenTag.className.replace(/\bpseudo_fullscreen\b/g, '');
|
|
playerInstance.fullscreenOff(fullscreenButton, menuOptionFullscreen);
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Prepares VAST for instant ads
|
|
*
|
|
* @param roll
|
|
*/
|
|
playerInstance.prepareVast = (roll) => {
|
|
let list = playerInstance.findRoll(roll);
|
|
|
|
for (let i = 0; i < list.length; i++) {
|
|
const rollListId = list[i];
|
|
|
|
if (!(playerInstance.rollsById[rollListId].vastLoaded !== true && playerInstance.rollsById[rollListId].error !== true)) {
|
|
continue;
|
|
}
|
|
|
|
playerInstance.processVastWithRetries(playerInstance.rollsById[rollListId]);
|
|
}
|
|
};
|
|
|
|
playerInstance.playMainVideoWhenVastFails = (errorCode) => {
|
|
playerInstance.debugMessage('playMainVideoWhenVastFails called');
|
|
playerInstance.domRef.player.removeEventListener('loadedmetadata', playerInstance.switchPlayerToVastMode);
|
|
playerInstance.domRef.player.pause();
|
|
playerInstance.toggleLoader(false);
|
|
playerInstance.displayOptions.vastOptions.vastAdvanced.noVastVideoCallback();
|
|
|
|
if (!playerInstance.vastOptions || typeof playerInstance.vastOptions.errorUrl === 'undefined') {
|
|
playerInstance.announceLocalError(errorCode);
|
|
} else {
|
|
playerInstance.announceError(errorCode);
|
|
}
|
|
|
|
playerInstance.switchToMainVideo();
|
|
};
|
|
|
|
// TODO: ???
|
|
playerInstance.switchPlayerToVastMode = () => {
|
|
};
|
|
|
|
/**
|
|
* Process the XML response
|
|
*
|
|
* @param ad
|
|
*/
|
|
function processAdCreatives(ad) {
|
|
const adElement = ad.data;
|
|
|
|
if (!adElement) {
|
|
return;
|
|
}
|
|
|
|
const creativeElements = Array.from(adElement.getElementsByTagName('Creative'));
|
|
|
|
if (creativeElements.length) {
|
|
for (let i = 0; i < creativeElements.length; i++) {
|
|
const creativeElement = creativeElements[i];
|
|
|
|
try {
|
|
if (ad.adType === 'linear') {
|
|
const linearCreatives = creativeElement.getElementsByTagName('Linear');
|
|
const creativeLinear = linearCreatives[0];
|
|
|
|
//Extract the Ad data if it is actually the Ad (!wrapper)
|
|
if (!playerInstance.hasVastAdTagUri(adElement) && playerInstance.hasInLine(adElement)) {
|
|
//Set initial values
|
|
ad.adFinished = false;
|
|
ad.vpaid = false;
|
|
|
|
//Extract the necessary data from the Linear node
|
|
ad.skipoffset = playerInstance.convertTimeStringToSeconds(creativeLinear.getAttribute('skipoffset'));
|
|
ad.clickthroughUrl = playerInstance.getClickThroughUrlFromLinear(creativeLinear);
|
|
ad.duration = playerInstance.getDurationFromLinear(creativeLinear);
|
|
ad.mediaFileList = playerInstance.getMediaFileListFromLinear(creativeLinear);
|
|
ad.adParameters = playerInstance.getAdParametersFromLinear(creativeLinear);
|
|
ad.iconClick = ad.iconClick || playerInstance.getIconClickThroughFromLinear(creativeLinear);
|
|
|
|
if (ad.adParameters) {
|
|
ad.vpaid = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (ad.adType === 'nonLinear') {
|
|
const nonLinearCreatives = creativeElement.getElementsByTagName('NonLinearAds');
|
|
const creativeNonLinear = nonLinearCreatives[0];
|
|
|
|
//Extract the Ad data if it is actually the Ad (!wrapper)
|
|
if (!playerInstance.hasVastAdTagUri(adElement) && playerInstance.hasInLine(adElement)) {
|
|
//Set initial values
|
|
ad.vpaid = false;
|
|
|
|
//Extract the necessary data from the NonLinear node
|
|
ad.clickthroughUrl = playerInstance.getClickThroughUrlFromNonLinear(creativeNonLinear);
|
|
ad.duration = playerInstance.getDurationFromNonLinear(creativeNonLinear); // VAST version < 4.0
|
|
ad.dimension = playerInstance.getDimensionFromNonLinear(creativeNonLinear); // VAST version < 4.0
|
|
ad.staticResource = playerInstance.getStaticResourceFromNonLinear(creativeNonLinear);
|
|
ad.creativeType = playerInstance.getCreativeTypeFromStaticResources(creativeNonLinear);
|
|
ad.adParameters = playerInstance.getAdParametersFromLinear(creativeNonLinear);
|
|
|
|
if (ad.adParameters) {
|
|
ad.vpaid = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Current support is for only one creative element
|
|
// break the loop if creative was successful
|
|
break;
|
|
} catch (err) {
|
|
if (creativeElement.firstElementChild &&
|
|
!(creativeElement.firstElementChild.tagName === 'Linear' ||
|
|
creativeElement.firstElementChild.tagName === 'NonLinearAds')) {
|
|
console.warn('Skipping ' + creativeElement.firstElementChild.tagName + ', this might not be supported yet.')
|
|
}
|
|
console.error(err);
|
|
}
|
|
};
|
|
}
|
|
|
|
return ad;
|
|
}
|
|
|
|
/**
|
|
* Parse the VAST Tag
|
|
*
|
|
* @param vastObj
|
|
*/
|
|
playerInstance.processVastWithRetries = (vastObj) => {
|
|
let vastTag = vastObj.vastTag;
|
|
const rollListId = vastObj.id;
|
|
|
|
playerInstance.domRef.player.addEventListener('adId_' + rollListId, playerInstance[vastObj.roll]);
|
|
|
|
const handleVastResult = function (pass, adOptionsList) {
|
|
if (pass && Array.isArray(adOptionsList) && !playerInstance.displayOptions.vastOptions.allowVPAID && adOptionsList.some(adOptions => adOptions.vpaid)) {
|
|
adOptionsList = adOptionsList.filter(adOptions => adOptions.vpaid !== true);
|
|
playerInstance.announceLocalError('103', 'VPAID not allowed, so skipping this VAST tag.')
|
|
}
|
|
|
|
if (pass && Array.isArray(adOptionsList) && adOptionsList.length) {
|
|
|
|
playerInstance.adPool[rollListId] = [];
|
|
|
|
adOptionsList.forEach((tmpOptions, index) => {
|
|
tmpOptions.id = rollListId + '_AD' + index;
|
|
tmpOptions.rollListId = rollListId;
|
|
|
|
if (tmpOptions.adType === 'linear') {
|
|
|
|
if ((typeof tmpOptions.iconClick !== 'undefined') && (tmpOptions.iconClick !== null) && tmpOptions.iconClick.length) {
|
|
tmpOptions.landingPage = tmpOptions.iconClick;
|
|
}
|
|
|
|
const selectedMediaFile = playerInstance.getSupportedMediaFileObject(tmpOptions.mediaFileList);
|
|
if (selectedMediaFile) {
|
|
tmpOptions.mediaType = selectedMediaFile.mediaType;
|
|
}
|
|
|
|
}
|
|
|
|
tmpOptions.adType = tmpOptions.adType ? tmpOptions.adType : 'unknown';
|
|
playerInstance.adPool[rollListId].push(Object.assign({}, tmpOptions));
|
|
|
|
if (playerInstance.hasTitle()) {
|
|
const title = playerInstance.domRef.wrapper.querySelector('.fp_title');
|
|
title.style.display = 'none';
|
|
}
|
|
|
|
playerInstance.rollsById[rollListId].ads.push(tmpOptions);
|
|
});
|
|
|
|
playerInstance.rollsById[rollListId].vastLoaded = true;
|
|
|
|
const event = document.createEvent('Event');
|
|
|
|
event.initEvent('adId_' + rollListId, false, true);
|
|
playerInstance.domRef.player.dispatchEvent(event);
|
|
playerInstance.displayOptions.vastOptions.vastAdvanced.vastLoadedCallback();
|
|
} else {
|
|
// when vast failed
|
|
playerInstance.announceLocalError('101');
|
|
|
|
if (vastObj.hasOwnProperty('fallbackVastTags') && vastObj.fallbackVastTags.length > 0) {
|
|
vastTag = vastObj.fallbackVastTags.shift();
|
|
playerInstance.processUrl(vastTag, handleVastResult, rollListId);
|
|
} else {
|
|
if (vastObj.roll === 'preRoll') {
|
|
playerInstance.preRollFail(vastObj);
|
|
}
|
|
playerInstance.rollsById[rollListId].error = true;
|
|
}
|
|
}
|
|
};
|
|
|
|
playerInstance.processUrl(vastTag, handleVastResult, rollListId);
|
|
};
|
|
|
|
playerInstance.processUrl = (vastTag, callBack, rollListId) => {
|
|
const numberOfRedirects = 0;
|
|
|
|
const tmpOptions = {
|
|
tracking: [],
|
|
stopTracking: [],
|
|
impression: [],
|
|
viewImpression: [],
|
|
clicktracking: [],
|
|
vastLoaded: false
|
|
};
|
|
|
|
playerInstance.resolveVastTag(
|
|
vastTag,
|
|
numberOfRedirects,
|
|
tmpOptions,
|
|
callBack,
|
|
rollListId
|
|
);
|
|
};
|
|
|
|
/**
|
|
* Gets first stand-alone ad
|
|
*
|
|
* @param {Array<RawAdTree>} ads
|
|
* @returns {Array<RawAdTree>}
|
|
*/
|
|
function getFirstStandAloneAd(ads) {
|
|
for (const ad of ads) {
|
|
const isAdPod = ad.data.attributes.sequence !== undefined;
|
|
|
|
if (!isAdPod) {
|
|
return [ad];
|
|
}
|
|
}
|
|
|
|
return [];
|
|
}
|
|
|
|
/**
|
|
* Resolves ad requests recursively and returns a tree of "Ad" and "Wrapper" elements
|
|
*
|
|
* @param {string} url vast resource url
|
|
* @param {number} maxDepth depth of recursive calls (wrapper depth)
|
|
* @param {Partial<RawAdTree>} baseNode used for recursive calls as base node
|
|
* @param {number} currentDepth used internally to track depth
|
|
* @param {boolean} followAdditionalWrappers used internally to track nested wrapper calls
|
|
* @returns {Promise<RawAdTree>}
|
|
*/
|
|
async function resolveAdTreeRequests(url, maxDepth, baseNode = {}, currentDepth = 0, followAdditionalWrappers = true) {
|
|
const adTree = { ...baseNode, children: [] };
|
|
const { responseXML } = await playerInstance.sendRequestAsync(url, true, playerInstance.displayOptions.vastOptions.vastTimeout);
|
|
const adElements = Array.from(responseXML.getElementsByTagName('Ad'));
|
|
|
|
for (const adElement of adElements) {
|
|
const vastAdTagUri = playerInstance.getVastAdTagUriFromWrapper(adElement);
|
|
const isAdPod = adElement.attributes.sequence !== undefined;
|
|
const adNode = { data: adElement };
|
|
|
|
if (vastAdTagUri && currentDepth <= maxDepth && followAdditionalWrappers) {
|
|
const [wrapperElement] = adElement.getElementsByTagName('Wrapper');
|
|
const disableAdditionalWrappers = wrapperElement.attributes.followAdditionalWrappers && ["false", "0"].includes(wrapperElement.attributes.followAdditionalWrappers.value); // See VAST Wrapper spec
|
|
const allowMultipleAds = wrapperElement.attributes.allowMultipleAds && ["true", "1"].includes(wrapperElement.attributes.allowMultipleAds.value); // See VAST Wrapper spec
|
|
const fallbackOnNoAd = wrapperElement.attributes.fallbackOnNoAd && ["true", "1"].includes(wrapperElement.attributes.fallbackOnNoAd.value);
|
|
|
|
try {
|
|
const wrapperResponse = await resolveAdTreeRequests(vastAdTagUri, maxDepth, { tagType: 'wrapper', ...adNode, fallbackOnNoAd }, currentDepth+1, !disableAdditionalWrappers);
|
|
wrapperResponse.fallbackOnNoAd = fallbackOnNoAd;
|
|
|
|
if (!allowMultipleAds || isAdPod) {
|
|
wrapperResponse.children = getFirstStandAloneAd(wrapperResponse.children);
|
|
}
|
|
|
|
adTree.children.push(wrapperResponse);
|
|
} catch (e) {
|
|
adTree.children.push({ tagType: `wrapper`, fallbackOnNoAd, httpError: true })
|
|
playerInstance.debugMessage(`Error when loading Wrapper, will trigger fallback if available`, e);
|
|
}
|
|
} else if (!vastAdTagUri) {
|
|
let mediaFileIsValid = true;
|
|
let mediaFileUrl = '';
|
|
if (Array.from(adElement.getElementsByTagName('AdParameters')).length) {
|
|
const mediaFiles = Array.from(adElement.getElementsByTagName('AdParameters'));
|
|
mediaFileIsValid = false;
|
|
for (const mediaFile of mediaFiles) {
|
|
mediaFileUrl = mediaFile.textContent.trim();
|
|
try {
|
|
const mediaFileObj = JSON.parse(mediaFileUrl);
|
|
mediaFileUrl = mediaFileObj.videos[0].url;
|
|
} catch (error) {
|
|
console.error("Error parsing media file URL:", error);
|
|
}
|
|
}
|
|
} else if (Array.from(adElement.getElementsByTagName('MediaFiles')).length) {
|
|
const mediaFiles = Array.from(adElement.getElementsByTagName('MediaFiles'));
|
|
const mediaFile = mediaFiles[0].getElementsByTagName('MediaFile');
|
|
mediaFileIsValid = false;
|
|
for (const mediaFileTemp of mediaFile) {
|
|
mediaFileUrl = mediaFileTemp.textContent.trim();
|
|
}
|
|
};
|
|
mediaFileIsValid = await validateMediaFile(mediaFileUrl);
|
|
if (mediaFileIsValid) {
|
|
adTree.children.push({ tagType: 'inLine', mediaFileUrl, ...adNode });
|
|
break;
|
|
} else {
|
|
adTree.children.push({ tagType: 'inLine', mediaError: true, ...adNode });
|
|
playerInstance.debugMessage(`No valid media file found in Inline ad.`);
|
|
}
|
|
}
|
|
}
|
|
|
|
return adTree;
|
|
}
|
|
|
|
|
|
/**
|
|
* Validate Media File to check if videos play
|
|
*
|
|
* @param {mediaFileUrl}
|
|
*/
|
|
async function validateMediaFile(mediaFileUrl) {
|
|
try {
|
|
const response = await fetch(mediaFileUrl);
|
|
if (!response.ok || response.headers.get('content-type').indexOf('video') === -1) {
|
|
return false;
|
|
}
|
|
const videoElement = document.createElement('video');
|
|
videoElement.src = mediaFileUrl;
|
|
const canPlay = await videoElement.canPlayType(response.headers.get('content-type'));
|
|
return canPlay !== "";
|
|
} catch (error) {
|
|
console.error('Failed to load media file:', error);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Transforms an Ad Tree to a 1-dimensional array of Ads with wrapper data attached to each ad
|
|
*
|
|
* @param {RawAdTree} root
|
|
* @param {Array<RawAd>} ads
|
|
* @param {Array<XMLDocument>} wrappers
|
|
* @returns {Array<RawAd>}
|
|
*/
|
|
function flattenAdTree(root, ads = [], wrappers = []) {
|
|
const currentWrappers = [...wrappers, root.data];
|
|
|
|
if (Array.isArray(root.children) && root.children.length) {
|
|
root.children.forEach(child => flattenAdTree(child, ads, currentWrappers));
|
|
}
|
|
|
|
if (root.tagType === 'inLine') {
|
|
ads.push({ ...root, wrappers: currentWrappers.filter(Boolean) });
|
|
}
|
|
|
|
return ads;
|
|
}
|
|
|
|
/**
|
|
* Register Ad element properties to an Ad based on its data and its wrapper data if available
|
|
*
|
|
* @param {RawAd} rawAd
|
|
* @param {{ tracking: Array, stopTracking: Array, impression: Array, viewImpression: Array, clicktracking: Array }} options
|
|
* @returns {Ad}
|
|
*/
|
|
function registerAdProperties(rawAd, options) {
|
|
const ad = { ...rawAd, ...JSON.parse(JSON.stringify(options)) };
|
|
|
|
ad.adType = (ad.data.getElementsByTagName('Linear').length && 'linear') ||
|
|
(ad.data.getElementsByTagName('NonLinearAds').length && 'nonLinear') || 'unknown';
|
|
|
|
[...(ad.wrappers || []), ad.data].filter(Boolean).forEach(dataSource => {
|
|
// Register impressions
|
|
const impression = dataSource.getElementsByTagName('Impression');
|
|
if (impression !== null) {
|
|
playerInstance.registerImpressionEvents(impression, ad);
|
|
}
|
|
|
|
// Register viewable impressions
|
|
const viewableImpression = dataSource.getElementsByTagName('Viewable');
|
|
if (viewableImpression !== null) {
|
|
playerInstance.registerViewableImpressionEvents(viewableImpression, ad);
|
|
}
|
|
|
|
// Get the error tag, if any
|
|
const errorTags = dataSource.getElementsByTagName('Error');
|
|
if (errorTags !== null) {
|
|
playerInstance.registerErrorEvents(errorTags, ad);
|
|
}
|
|
|
|
// Sets CTA from vast
|
|
const [titleCta] = dataSource.getElementsByTagName('TitleCTA');
|
|
if (titleCta) {
|
|
playerInstance.setCTAFromVast(titleCta, ad);
|
|
}
|
|
|
|
// Register tracking events
|
|
playerInstance.registerTrackingEvents(dataSource, ad);
|
|
const clickTracks = ad.adType === 'linear' ?
|
|
playerInstance.getClickTrackingEvents(dataSource) :
|
|
playerInstance.getNonLinearClickTrackingEvents(dataSource);
|
|
playerInstance.registerClickTracking(clickTracks, ad);
|
|
});
|
|
|
|
ad.sequence = ad.data.attributes.sequence ? Number(ad.data.attributes.sequence.value) : null;
|
|
ad.played = false;
|
|
|
|
return ad;
|
|
}
|
|
|
|
/**
|
|
* Handles selection of ad pod or standalone ad to be played
|
|
*
|
|
* @param {Array<Ad>} ads
|
|
* @param {number} maxDuration
|
|
* @param {number} maxQuantity
|
|
* @param {boolean} forceStandAloneAd
|
|
*/
|
|
function getPlayableAds(ads, maxDuration, maxQuantity, forceStandAloneAd) {
|
|
const { adPod } = ads
|
|
.filter(ad => Boolean(ad.sequence))
|
|
.sort((adX, adY) => adX.sequence - adY.sequence)
|
|
.reduce((playableAds, ad) => {
|
|
if (playableAds.adPod.length < maxQuantity && (playableAds.totalDuration + ad.duration) <= maxDuration) {
|
|
playableAds.adPod.push(ad);
|
|
}
|
|
|
|
return playableAds;
|
|
}, { adPod: [], totalDuration: 0 });
|
|
const adBuffet = ads.filter(ad => !Boolean(ad.sequence) && ad.duration < maxDuration);
|
|
|
|
const isValidAdPodFormats = adPod.map(ad => ad.adType).slice(0, -1).every(adType => adType === 'linear');
|
|
|
|
if (adPod.length > 0 && !forceStandAloneAd && isValidAdPodFormats) {
|
|
playerInstance.debugMessage('Playing valid adPod', adPod);
|
|
return adPod;
|
|
} else {
|
|
playerInstance.debugMessage('Trying to play single ad, adBuffet:', adBuffet);
|
|
return adBuffet.length > 0 ? [adBuffet[0]] : [];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param vastTag
|
|
* @param numberOfRedirects
|
|
* @param tmpOptions
|
|
* @param callback
|
|
* @param rollListId
|
|
*/
|
|
playerInstance.resolveVastTag = (vastTag, numberOfRedirects, tmpOptions, callback, rollListId) => {
|
|
if (!vastTag || vastTag === '') {
|
|
return callback(false);
|
|
}
|
|
|
|
resolveAdTreeRequests(vastTag, playerInstance.displayOptions.vastOptions.maxAllowedVastTagRedirects)
|
|
.then(result => {
|
|
try {
|
|
/** @see VAST 4.0 Wrapper.fallbackOnNoAd */
|
|
const triggerFallbackOnNoAd = result.children.some(ad =>
|
|
ad.tagType === 'wrapper' && ad.fallbackOnNoAd && (!/"tagType":"ad"/.test(JSON.stringify(ad)) || ad.httpError)
|
|
);
|
|
|
|
if (triggerFallbackOnNoAd) {
|
|
playerInstance.debugMessage('Error on VAST Wrapper, triggering fallbackOnNoAd. Ad tree:', result);
|
|
}
|
|
|
|
result = flattenAdTree(result).map(ad => processAdCreatives(registerAdProperties(ad, tmpOptions)));
|
|
|
|
const playableAds = getPlayableAds(
|
|
result,
|
|
playerInstance.rollsById[rollListId].maxTotalDuration || Number.MAX_SAFE_INTEGER,
|
|
playerInstance.rollsById[rollListId].maxTotalQuantity || Number.MAX_SAFE_INTEGER,
|
|
triggerFallbackOnNoAd,
|
|
);
|
|
|
|
(playableAds && playableAds.length) ? callback(true, playableAds) : callback(false);
|
|
} catch (error) {
|
|
callback(false);
|
|
}
|
|
})
|
|
.catch(() => {
|
|
return callback(false);
|
|
});
|
|
};
|
|
|
|
playerInstance.setVastList = () => {
|
|
const rolls = {};
|
|
const rollsGroupedByType = { preRoll: [], postRoll: [], midRoll: [], onPauseRoll: [] };
|
|
const def = {
|
|
id: null,
|
|
roll: null,
|
|
vastLoaded: false,
|
|
error: false,
|
|
adText: null,
|
|
adTextPosition: null,
|
|
};
|
|
let idPart = 0;
|
|
|
|
const validateVastList = function (item) {
|
|
let hasError = false;
|
|
|
|
if (item.roll === 'midRoll') {
|
|
if (typeof item.timer === 'undefined') {
|
|
hasError = true;
|
|
}
|
|
}
|
|
|
|
return hasError;
|
|
};
|
|
|
|
const validateRequiredParams = function (item) {
|
|
let hasError = false;
|
|
|
|
if (!item.vastTag) {
|
|
playerInstance.announceLocalError(102, '"vastTag" property is missing from adList.');
|
|
hasError = true;
|
|
}
|
|
|
|
if (!item.roll) {
|
|
playerInstance.announceLocalError(102, '"roll" is missing from adList.');
|
|
hasError = true;
|
|
}
|
|
|
|
if (playerInstance.availableRolls.indexOf(item.roll) === -1) {
|
|
playerInstance.announceLocalError(102, 'Only ' + playerInstance.availableRolls.join(',') + ' rolls are supported.');
|
|
hasError = true;
|
|
}
|
|
|
|
if (item.size && playerInstance.supportedNonLinearAd.indexOf(item.size) === -1) {
|
|
playerInstance.announceLocalError(102, 'Only ' + playerInstance.supportedNonLinearAd.join(',') + ' size are supported.');
|
|
hasError = true;
|
|
}
|
|
|
|
return hasError;
|
|
};
|
|
|
|
if (playerInstance.displayOptions.vastOptions.hasOwnProperty('adList')) {
|
|
|
|
for (let key in playerInstance.displayOptions.vastOptions.adList) {
|
|
|
|
let rollItem = playerInstance.displayOptions.vastOptions.adList[key];
|
|
|
|
if (validateRequiredParams(rollItem)) {
|
|
playerInstance.announceLocalError(102, 'Wrong adList parameters.');
|
|
continue;
|
|
}
|
|
const id = 'ID' + idPart;
|
|
|
|
rolls[id] = Object.assign({}, def);
|
|
rolls[id] = Object.assign(rolls[id], playerInstance.displayOptions.vastOptions.adList[key]);
|
|
if (rollItem.roll === 'midRoll') {
|
|
rolls[id].error = validateVastList('midRoll', rollItem);
|
|
}
|
|
rolls[id].id = id;
|
|
rolls[id].ads = [];
|
|
idPart++;
|
|
|
|
}
|
|
}
|
|
|
|
// group the ads by roll
|
|
// pushing object references and forming json
|
|
Object.keys(rolls).map(function (e) {
|
|
switch (rolls[e].roll.toLowerCase()) {
|
|
case 'preRoll'.toLowerCase():
|
|
rollsGroupedByType.preRoll.push(rolls[e]);
|
|
break;
|
|
case 'midRoll'.toLowerCase():
|
|
rollsGroupedByType.midRoll.push(rolls[e]);
|
|
break;
|
|
case 'postRoll'.toLowerCase():
|
|
rollsGroupedByType.postRoll.push(rolls[e]);
|
|
break;
|
|
case 'onPauseRoll'.toLowerCase():
|
|
rollsGroupedByType.onPauseRoll.push(rolls[e]);
|
|
break;
|
|
default:
|
|
console.error(`${rolls[e].roll.toLowerCase()} is not a recognized roll`);
|
|
break;
|
|
}
|
|
});
|
|
|
|
playerInstance.adGroupedByRolls = rollsGroupedByType;
|
|
playerInstance.rollsById = rolls;
|
|
};
|
|
|
|
playerInstance.onVastAdEnded = (event) => {
|
|
if (event) {
|
|
event.stopImmediatePropagation();
|
|
}
|
|
playerInstance.vastOptions.adFinished = true;
|
|
//"this" is the HTML5 video tag, because it dispatches the "ended" event
|
|
playerInstance.deleteVastAdElements();
|
|
playerInstance.checkForNextAd();
|
|
};
|
|
|
|
playerInstance.vastLogoBehaviour = (vastPlaying) => {
|
|
if (!playerInstance.displayOptions.layoutControls.logo.showOverAds) {
|
|
const logoHolder = playerInstance.domRef.wrapper.querySelector('.logo_holder');
|
|
|
|
if (!logoHolder) {
|
|
return;
|
|
}
|
|
|
|
logoHolder.style.display = vastPlaying ? 'none' : 'inline';
|
|
}
|
|
};
|
|
|
|
playerInstance.deleteVastAdElements = () => {
|
|
playerInstance.removeClickthrough();
|
|
playerInstance.removeSkipButton();
|
|
playerInstance.removeAdCountdown();
|
|
playerInstance.removeAdPlayingText();
|
|
playerInstance.removeCTAButton();
|
|
playerInstance.vastLogoBehaviour(false);
|
|
};
|
|
}
|
|
|
|
export default vast
|