UNPKG

videojs-contrib-ads

Version:

A framework that provides common functionality needed by video advertisement libraries working with video.js.

241 lines (199 loc) 8.91 kB
/* This feature provides an optional method for ad plugins to insert run-time values into an ad server URL or configuration. */ import window from 'global/window'; import document from 'global/document'; import videojs from 'video.js'; import {tcData} from './tcf.js'; import {getCurrentUspString} from './usPrivacy.js'; const uriEncodeIfNeeded = function(value, uriEncode) { return uriEncode ? encodeURIComponent(value) : value; }; // Add custom field macros to macros object // based on given name for custom fields property of mediainfo object. const customFields = function(mediainfo, macros, customFieldsName) { if (mediainfo && mediainfo[customFieldsName]) { const fields = mediainfo[customFieldsName]; const fieldNames = Object.keys(fields); for (let i = 0; i < fieldNames.length; i++) { const tag = '{mediainfo.' + customFieldsName + '.' + fieldNames[i] + '}'; macros[tag] = fields[fieldNames[i]]; } } }; const getMediaInfoMacros = function(mediainfo, defaults) { const macros = {}; ['description', 'tags', 'reference_id', 'ad_keys'].forEach((prop) => { if (mediainfo && mediainfo[prop]) { macros[`{mediainfo.${prop}}`] = mediainfo[prop]; } else if (defaults[`{mediainfo.${prop}}`]) { macros[`{mediainfo.${prop}}`] = defaults[`{mediainfo.${prop}}`]; } else { macros[`{mediainfo.${prop}}`] = ''; } }); ['custom_fields', 'customFields'].forEach((customFieldProp) => { customFields(mediainfo, macros, customFieldProp); }); return macros; }; const getDefaultValues = function(string) { const defaults = {}; const modifiedString = string.replace(/{([^}=]+)=([^}]*)}/g, (match, name, defaultVal) => { defaults[`{${name}}`] = defaultVal; return `{${name}}`; }); return {defaults, modifiedString}; }; const getStaticMacros = function(player) { return { '{player.id}': player.options_['data-player'] || player.id_, '{player.height}': player.currentHeight(), '{player.width}': player.currentWidth(), '{player.heightInt}': Math.round(player.currentHeight()), '{player.widthInt}': Math.round(player.currentWidth()), '{player.autoplay}': player.autoplay() ? 1 : 0, '{player.muted}': player.muted() ? 1 : 0, '{player.language}': player.language() || '', '{mediainfo.id}': player.mediainfo ? player.mediainfo.id : '', '{mediainfo.name}': player.mediainfo ? player.mediainfo.name : '', '{mediainfo.duration}': player.mediainfo ? player.mediainfo.duration : '', '{player.duration}': player.duration(), '{player.durationInt}': Math.round(player.duration()), '{player.live}': player.duration() === Infinity ? 1 : 0, '{player.pageUrl}': videojs.dom.isInFrame() ? document.referrer : window.location.href, '{playlistinfo.id}': player.playlistinfo ? player.playlistinfo.id : '', '{playlistinfo.name}': player.playlistinfo ? player.playlistinfo.name : '', '{timestamp}': new Date().getTime(), '{document.referrer}': document.referrer, '{window.location.href}': window.location.href, '{random}': Math.floor(Math.random() * 1000000000000) }; }; const getTcfMacros = function(tcDataObj) { const tcfMacros = {}; Object.keys(tcDataObj).forEach((key) => { tcfMacros[`{tcf.${key}}`] = tcDataObj[key]; }); tcfMacros['{tcf.gdprAppliesInt}'] = tcDataObj.gdprApplies ? 1 : 0; return tcfMacros; }; const getUspMacros = () => { return {'{usp.uspString}': getCurrentUspString()}; }; // This extracts and evaluates variables from the `window` object for macro replacement. While replaceMacros() handles generic macro name // overriding for other macro types, this function also needs to reference the overrides in order to map custom macro names in the string // to their corresponding default pageVariable names, so they can be evaluated on the `window` and stored for later replacement in replaceMacros(). const getPageVariableMacros = function(string, defaults, macroNameOverrides) { const pageVarRegex = new RegExp('{pageVariable\\.([^}]+)}', 'g'); const pageVariablesMacros = {}; // Aggregate any default pageVariable macros found in the string with any pageVariable macros that have custom names specified in // macroNameOverrides. const pageVariables = (string.match(pageVarRegex) || []).concat(Object.keys(macroNameOverrides) .filter(macroName => pageVarRegex.test(macroName) && string.includes(macroNameOverrides[macroName]))); if (!pageVariables) { return; } pageVariables.forEach((pageVar) => { const key = pageVar; const name = pageVar.slice(14, -1); const names = name.split('.'); let context = window; let value; // Iterate down multiple levels of selector without using eval // This makes things like pageVariable.foo.bar work for (let i = 0; i < names.length; i++) { if (i === names.length - 1) { value = context[names[i]]; } else { context = context[names[i]]; if (typeof context === 'undefined') { break; } } } const type = typeof value; // Only allow certain types of values. Anything else is probably a mistake. if (value === null) { pageVariablesMacros[key] = 'null'; } else if (value === undefined) { if (defaults[key]) { pageVariablesMacros[key] = defaults[key]; } else { videojs.log.warn(`Page variable "${name}" not found`); pageVariablesMacros[key] = ''; } } else if (type !== 'string' && type !== 'number' && type !== 'boolean') { videojs.log.warn(`Page variable "${name}" is not a supported type`); pageVariablesMacros[key] = ''; } else { pageVariablesMacros[key] = value; } }); return pageVariablesMacros; }; const replaceMacros = function(string, macros, uriEncode, overrides = {}) { for (const macroName in macros) { // The resolvedMacroName is the macro as it is expected to appear in the actual string, or regex if it has been provided. const resolvedMacroName = overrides.hasOwnProperty(macroName) ? overrides[macroName] : macroName; if (resolvedMacroName.startsWith('r:')) { try { const regex = new RegExp(resolvedMacroName.slice(2), 'g'); string = string.replace(regex, uriEncodeIfNeeded(macros[macroName], uriEncode)); } catch (error) { videojs.log.warn(`Unable to replace macro with regex "${resolvedMacroName}". The provided regex may be invalid.`); } } else { string = string.split(resolvedMacroName).join(uriEncodeIfNeeded(macros[macroName], uriEncode)); } } return string; }; /** * * @param {string} string * Any string with macros to be replaced * @param {boolean} uriEncode * A Boolean value indicating whether the macros should be replaced with URI-encoded values * @param {object} customMacros * An object with custom macros and values to map them to. For example: {'{five}': 5} * @param {boolean} customMacros.disableDefaultMacros * A boolean indicating whether replacement of default macros should be forgone in favor of only customMacros * @param {object} customMacros.macroNameOverrides * An object that specifies custom names for default macros, following the following format: * // {'{default-macro-name}': '{new-macro-name}'} * {'{player.id}': '{{PLAYER_ID}}', ...} * @returns {string} * The provided string with all macros replaced. For example: adMacroReplacement('{player.id}') returns a string of the player id */ export default function adMacroReplacement(string, uriEncode = false, customMacros = {}) { const disableDefaultMacros = customMacros.disableDefaultMacros || false; const macroNameOverrides = customMacros.macroNameOverrides || {}; // Remove special properties from customMacros delete customMacros.disableDefaultMacros; delete customMacros.macroNameOverrides; const macros = customMacros; if (disableDefaultMacros) { return replaceMacros(string, macros, uriEncode, macroNameOverrides); } // Get macros with defaults e.g. {x=y}, store the values in `defaults` and replace with standard macros in the string const {defaults, modifiedString} = getDefaultValues(string); string = modifiedString; // Get all macro values Object.assign( macros, getStaticMacros(this), getMediaInfoMacros(this.mediainfo, defaults), getTcfMacros(tcData), getUspMacros(), getPageVariableMacros(string, defaults, macroNameOverrides) ); // Perform macro replacement string = replaceMacros(string, macros, uriEncode, macroNameOverrides); // Replace any remaining default values that have not already been replaced. This includes mediainfo custom fields. for (const macro in defaults) { string = string.replace(macro, defaults[macro]); } return string; }