video-ad-sdk
Version:
VAST/VPAID SDK that allows video ads to be played on top of any player
541 lines (468 loc) • 15.1 kB
text/typescript
import {
get,
getAll,
getFirstChild,
getText,
getAttributes,
getAttribute
} from '../xml'
import type {
ParsedAd,
ParsedXML,
VastChain,
VastMacro,
WrapperOptions,
MediaFile,
InteractiveFile,
ParsedOffset,
VpaidCreativeData,
Optional
} from '../types'
import {parseOffset} from './helpers/parseOffset'
import {getLinearCreative} from './helpers/getLinearCreative'
import {getLinearTrackingEvents} from './getLinearTrackingEvents'
import {getNonLinearTrackingEvents} from './getNonLinearTrackingEvents'
import {getIcons} from './getIcons'
const getBooleanValue = (value: unknown): boolean => {
if (typeof value === 'string') {
return value === 'true'
}
return Boolean(value)
}
const compareBySequence = (itemA: ParsedXML, itemB: ParsedXML): number => {
const itemASequenceString = getAttribute(itemA, 'sequence')
const itemBSequenceString = getAttribute(itemB, 'sequence')
const itemASequence = itemASequenceString && parseInt(itemASequenceString, 10)
const itemBSequence = itemBSequenceString && parseInt(itemBSequenceString, 10)
if (typeof itemASequence !== 'string' || typeof itemBSequence !== 'string') {
return 0
}
if (itemASequence < itemBSequence) {
return -1
}
if (itemASequence > itemBSequence) {
return 1
}
return 0
}
/**
* Selects the ads of the passed VAST.
*
* @param parsedVast Parsed VAST xml.
* @returns Array of ads or empty array.
*/
export const getAds = (parsedVast?: ParsedXML): ParsedAd[] => {
const vastElement = parsedVast && get(parsedVast, 'VAST')
const ads = vastElement && getAll(vastElement, 'Ad')
if (ads && ads.length > 0) {
return ads
}
return []
}
/**
* Gets the Error URI of the passed parsed VAST xml.
*
* @param parsedVast Parsed VAST xml.
* @returns array of the Vast Error URI
*/
export const getVastErrorURI = (
parsedVast?: ParsedXML
): Optional<VastMacro[]> => {
const vastElement = parsedVast && get(parsedVast, 'VAST')
const errors = vastElement && getAll(vastElement, 'Error')
if (errors && errors.length > 0) {
return errors.map((error) => getText(error) ?? '').filter(Boolean)
}
}
/**
* Gets the sequence of the pod ad.
*
* @param ad Parsed ad definition object.
* @returns The pod ad sequence number.
*/
export const getPodAdSequence = (ad: ParsedAd): Optional<number> => {
const sequenceString = getAttribute(ad, 'sequence')
const sequence = sequenceString && parseInt(sequenceString, 10)
if (typeof sequence === 'number' && !isNaN(sequence)) {
return sequence
}
}
/**
* Checks if the passed ad definition is a pod ad.
*
* @param ad Parsed ad definition object.
* @returns Returns true if there the ad is a pod ad and false otherwise.
*/
export const isPodAd = (ad: ParsedAd): boolean => Boolean(getPodAdSequence(ad))
/**
* Checks if the passed array of ads have an ad pod.
*
* @param parsedVast Parsed VAST xml.
* @returns Returns true if there is an ad pod in the array and false otherwise.
*/
export const hasAdPod = (parsedVast?: ParsedXML): boolean => {
const ads = getAds(parsedVast)
return Array.isArray(ads) && ads.filter(isPodAd).length > 1
}
/**
* Returns true if the passed vastChain has an ad pod or false otherwise.
*
* @param vastChain Array of VAST responses. See `load` or `requestAd` for more info.
* @returns True if the vastChain contains an ad pod and false otherwise.
*/
export const isAdPod = (vastChain: VastChain = []): boolean =>
vastChain.map(({parsedXML}) => parsedXML).some(hasAdPod)
/**
* Selects the first ad of the passed VAST. If the passed VAST response contains an ad pod it will return the first ad in the ad pod sequence.
*
* @param parsedVast Parsed VAST xml.
* @returns First ad of the VAST xml.
*/
export const getFirstAd = (parsedVast?: ParsedXML): Optional<ParsedAd> => {
const ads = getAds(parsedVast)
if (Array.isArray(ads) && ads.length > 0) {
if (hasAdPod(parsedVast)) {
return ads.filter(isPodAd).sort(compareBySequence)[0]
}
return ads[0]
}
}
/**
* Checks if the passed ad is a Wrapper.
*
* @param ad VAST ad object.
* @returns `true` if the ad contains a wrapper and `false` otherwise.
*/
export const isWrapper = (ad?: ParsedAd): boolean =>
Boolean(get(ad || ({} as ParsedXML), 'Wrapper'))
/**
* Checks if the passed ad is an Inline.
*
* @param ad VAST ad object.
* @returns Returns `true` if the ad contains an Inline or `false` otherwise.
*/
export const isInline = (ad?: ParsedAd): boolean =>
Boolean(get(ad || ({} as ParsedXML), 'Inline'))
/**
* Returns the VASTAdTagURI from the wrapper ad.
*
* @param ad VAST ad object.
* @returns Returns the VASTAdTagURI from the wrapper ad.
*/
export const getVASTAdTagURI = (ad: ParsedAd): Optional<string> => {
const wrapperElement = get(ad, 'Wrapper')
const vastAdTagURIElement =
wrapperElement && get(wrapperElement, 'VastAdTagUri')
return vastAdTagURIElement && getText(vastAdTagURIElement)
}
/**
* Returns the options from the wrapper ad.
*
* @param ad VAST ad object.
* @returns Returns the options from the wrapper ad.
*/
export const getWrapperOptions = (ad: ParsedAd): WrapperOptions => {
const wrapperElement = get(ad, 'Wrapper')
const options: WrapperOptions = {}
if (wrapperElement) {
const {allowMultipleAds, fallbackOnNoAd, followAdditionalWrappers} =
getAttributes(wrapperElement)
if (allowMultipleAds) {
options.allowMultipleAds = getBooleanValue(allowMultipleAds)
}
if (fallbackOnNoAd) {
options.fallbackOnNoAd = getBooleanValue(fallbackOnNoAd)
}
if (followAdditionalWrappers) {
options.followAdditionalWrappers = getBooleanValue(
followAdditionalWrappers
)
}
}
return options
}
/**
* Gets the Error URI of the passed ad.
*
* @param ad VAST ad object.
* @returns array of the Vast ad Error URI.
*/
export const getAdErrorURI = (ad: ParsedAd): Optional<string[]> => {
const adTypeElement = ad && getFirstChild(ad)
const errors = adTypeElement && getAll(adTypeElement, 'Error')
if (errors && errors.length > 0) {
return errors.map((error) => getText(error) ?? '').filter(Boolean)
}
}
/**
* Gets array of the Impression URI of the passed ad.
*
* @param ad VAST ad object.
* @returns array of the Vast ad Impression URI.
*/
export const getImpression = (ad: ParsedAd): Optional<string[]> => {
const adTypeElement = ad && getFirstChild(ad)
const impressions = adTypeElement && getAll(adTypeElement, 'Impression')
if (impressions && impressions.length > 0) {
return impressions
.map((impression: ParsedXML) => getText(impression) ?? '')
.filter(Boolean)
}
}
/**
* Gets array of the Viewable URI of the passed ad.
*
* @param ad VAST ad object.
* @returns array of the Vast ad Viewable URI.
*/
export const getViewable = (ad: ParsedAd): Optional<string[]> => {
const adTypeElement = ad && getFirstChild(ad)
const viewableImpression =
adTypeElement && get(adTypeElement, 'ViewableImpression')
const viewableElements =
viewableImpression && getAll(viewableImpression, 'Viewable')
if (viewableElements && viewableElements.length > 0) {
return viewableElements
.map((element) => getText(element) ?? '')
.filter(Boolean)
}
}
/**
* Gets array of the NotViewable URI of the passed ad.
*
* @param ad VAST ad object.
* @returns array of the Vast ad NotViewable URI.
*/
export const getNotViewable = (ad: ParsedAd): Optional<string[]> => {
const adTypeElement = ad && getFirstChild(ad)
const viewableImpression =
adTypeElement && get(adTypeElement, 'ViewableImpression')
const notViewableElements =
viewableImpression && getAll(viewableImpression, 'NotViewable')
if (notViewableElements && notViewableElements.length > 0) {
return notViewableElements
.map((element) => getText(element) ?? '')
.filter(Boolean)
}
}
/**
* Gets array of the ViewUndetermined URI of the passed ad.
*
* @param ad VAST ad object.
* @returns array of the Vast ad ViewUndetermined URI.
*/
export const getViewUndetermined = (ad: ParsedAd): Optional<string[]> => {
const adTypeElement = ad && getFirstChild(ad)
const viewableImpression =
adTypeElement && get(adTypeElement, 'ViewableImpression')
const viewUndeterminedElements =
viewableImpression && getAll(viewableImpression, 'ViewUndetermined')
if (viewUndeterminedElements && viewUndeterminedElements.length > 0) {
return viewUndeterminedElements
.map((element) => getText(element) ?? '')
.filter(Boolean)
}
}
/**
* Gets the ad's MediaFiles.
*
* @param ad VAST ad object.
* @returns array of media files
*/
export const getMediaFiles = (ad: ParsedAd): Optional<MediaFile[]> => {
const creativeElement = ad && getLinearCreative(ad)
const universalAdIdElement =
creativeElement && get(creativeElement, 'UniversalAdId')
const universalAdId = universalAdIdElement && getText(universalAdIdElement)
const linearElement = creativeElement && get(creativeElement, 'Linear')
const mediaFilesElement = linearElement && get(linearElement, 'MediaFiles')
const mediaFileElements =
mediaFilesElement && getAll(mediaFilesElement, 'MediaFile')
if (mediaFileElements && mediaFileElements.length > 0) {
return mediaFileElements.map((mediaFileElement: ParsedXML) => {
const source = getText(mediaFileElement)
const {
apiFramework,
bitrate,
codec,
delivery,
height,
id,
maintainAspectRatio,
maxBitrate,
minBitrate,
scalable,
type,
width
} = getAttributes(mediaFileElement)
return {
apiFramework,
bitrate,
codec,
delivery,
height,
id,
maintainAspectRatio,
maxBitrate,
minBitrate,
scalable,
src: source,
type,
universalAdId,
width
}
})
}
}
/**
* Gets the ad's InteractiveFiles. That were added with the `InteractiveCreativeFile` tag.
*
* @param ad VAST ad object.
* @returns array of media files.
*/
export const getInteractiveCreativeFiles = (
ad: ParsedAd
): Optional<InteractiveFile[]> => {
const creativeElement = ad && getLinearCreative(ad)
const linearElement = creativeElement && get(creativeElement, 'Linear')
const mediaFilesElement = linearElement && get(linearElement, 'MediaFiles')
const interactiveElements =
mediaFilesElement && getAll(mediaFilesElement, 'InteractiveCreativeFile')
if (interactiveElements && interactiveElements.length > 0) {
return interactiveElements.map((interactiveElement: ParsedXML) => {
const {apiFramework, type} = getAttributes(interactiveElement)
const source = getText(interactiveElement)
return {
apiFramework,
src: source,
type
}
})
}
}
/**
* Gets all the ad's InteractiveFiles.
*
* @param ad VAST ad object.
* @returns array of media files
*/
export const getInteractiveFiles = (
ad: ParsedAd
): Optional<InteractiveFile[]> => {
let interactiveFiles = getInteractiveCreativeFiles(ad)
if (interactiveFiles) {
return interactiveFiles
}
const mediaFiles = getMediaFiles(ad)
if (mediaFiles) {
interactiveFiles = mediaFiles
.filter(({apiFramework = ''}) => apiFramework?.toLowerCase() === 'vpaid')
.map(({apiFramework, src: source, type}) => ({
apiFramework,
src: source,
type
}))
if (interactiveFiles.length > 0) {
return interactiveFiles
}
}
}
const getVideoClicksElement = (ad: ParsedAd): Optional<ParsedXML> => {
const creativeElement = ad && getLinearCreative(ad)
const linearElement = creativeElement && get(creativeElement, 'Linear')
return linearElement && get(linearElement, 'VideoClicks')
}
/**
* Gets the click through {@link VastMacro}.
*
* @param ad VAST ad object.
* @returns clickthrough macro
*/
export const getClickThrough = (ad: ParsedAd): Optional<VastMacro> => {
const videoClicksElement = getVideoClicksElement(ad)
const clickThroughElement =
videoClicksElement && get(videoClicksElement, 'ClickThrough')
return clickThroughElement && getText(clickThroughElement)
}
/**
* Gets the click through {@link VastMacro}.
*
* @param ad VAST ad object.
* @returns click tracking macro
*/
export const getClickTracking = (ad: ParsedAd): Optional<VastMacro[]> => {
const videoClicksElement = ad && getVideoClicksElement(ad)
const clickTrackingElements =
videoClicksElement && getAll(videoClicksElement, 'ClickTracking')
if (clickTrackingElements && clickTrackingElements.length > 0) {
return clickTrackingElements
.map((element) => getText(element) ?? '')
.filter(Boolean)
}
}
/**
* Gets the custom click {@link VastMacro}.
*
* @param ad VAST ad object.
* @returns click tracking macro
*/
export const getCustomClick = (ad: ParsedAd): Optional<VastMacro[]> => {
const videoClicksElement = getVideoClicksElement(ad)
const customClickElements =
videoClicksElement && getAll(videoClicksElement, 'CustomClick')
if (customClickElements && customClickElements.length > 0) {
return customClickElements
.map((element) => getText(element) ?? '')
.filter(Boolean)
}
}
/**
* Gets the skipoffset.
*
* @param ad VAST ad object.
* @returns the time offset in milliseconds or a string with the percentage
*/
export const getSkipOffset = (ad: ParsedAd): Optional<ParsedOffset> => {
const creativeElement = ad && getLinearCreative(ad)
const linearElement = creativeElement && get(creativeElement, 'Linear')
const skipoffset = linearElement && getAttribute(linearElement, 'skipoffset')
return skipoffset && parseOffset(skipoffset)
}
const getLinearContent = (xml: string): Optional<string> => {
const linearRegex = /<Linear([\s\S]*)<\/Linear/gm
const result = linearRegex.exec(xml)
return result?.[1]
}
const getAdParametersContent = (xml: string): Optional<string> => {
const paramsRegex = /<AdParameters[\s\w="]*>([\s\S]*)<\/AdParameters>/gm
const result = paramsRegex.exec(xml)
return (
result?.[1]
.replace(/[\n\s]*<!\[CDATA\[[\n\s]*/, '')
.replace(/[\n\s]*\]\]>[\n\s]*$/, '')
// unescape nested CDATA
.replace(/\]\]\]\]><!\[CDATA\[>/, ']]>')
.trim()
)
}
const getXmlEncodedValue = (xml: string): boolean => {
const xmlEncodedRegex = /<AdParameters[\s]*xmlEncoded="(.*?)">/gim
const result = xmlEncodedRegex.exec(xml)
return result?.[1] === 'true'
}
/**
* Gets the creative data.
*
* @param xml VAST XML text.
* @returns with `AdParameters` as they come in the XML and a flag `xmlEncoded` to indicate if the ad parameters are xml encoded.
*/
export const getCreativeData = (xml: string): VpaidCreativeData => {
const linearContent = getLinearContent(xml)
const AdParameters = linearContent && getAdParametersContent(linearContent)
const xmlEncoded = linearContent ? getXmlEncodedValue(linearContent) : false
return {
AdParameters,
xmlEncoded
}
}
export {getIcons, getLinearTrackingEvents, getNonLinearTrackingEvents}