@speechkit/speechkit-audio-player
Version:
A web player component that can play audio from https://speechkit.io
337 lines (304 loc) • 12.2 kB
JavaScript
import { fetchWithErrorHandling } from '../utils/BetterFetch'
import {
stringToXML,
getCreative,
getVASTEventURLs,
getNodesWithAttrValue,
getContentOfNodesWithAttrValue,
} from '../utils/ParseXML'
class AdsService {
constructor(options) {
this.apiKey = options.apiKey;
this.skBackend = options.skBackend;
this.projectCampaignId = options.projectCampaignId;
this.customAdMedia = options.customAdMedia;
this.ads = null;
this.adsIsPlayed = false;
this.adsIsPlaying = false;
this.googleAds = false;
this.adsHistory = null;
this.vastAdTag = options.vastAdTag;
this.GDPR = {gdprApplies: false};
this.getAds(options.vastAdTag);
}
// decide which type of ad to get, (this choice will be modified with full VAST integration)
getAds(vastAdTag) {
// call respective function: vast / spkt ads
if (!vastAdTag) {
return this.getSpktAds();
} else {
return this.getVastAd();
}
}
// Fetch Speechkit ads for specified campaign ID and return promise to ads object (JSON)
getSpktAds() {
if (!this.promise) {
this.promise = new Promise((resolve, reject) => {
// if JSON is passed in for ad previews, process in player
if(this.customAdMedia) {
this.ads = this.customAdMedia
return resolve(this.ads)
}
// exit if no projectCampaignId
if (!this.projectCampaignId) return resolve(this.ads);
// if we do have campaignId, fetch ads from API
const sUrl = `${this.skBackend}/api/v2/project_campaigns/${this.projectCampaignId}`
return fetchWithErrorHandling(sUrl, {
headers: {
'Authorization': `Token token=${this.apiKey}`,
}
})
// if response ok, return ads from body as json
.then(response => response.json())
// update the ads object and resolve the promise
.then(oJson => {
this.ads = oJson;
return resolve(this.ads);
})
.catch(error => {
reject();
console.log('Get Spkt Ad Error: '+ error);
});
});
}
return this.promise;
}
// to get CMP from URL parameter
getURLParameter(name) {
return decodeURIComponent((new RegExp('[?|&]' + name + '=' + '([^&;]+?)(&|#|;|$)')
.exec(location.search)||[,""])[1].replace(/\+/g, '%20'))||null;
}
loadScript(src) {
return new Promise((resolve, reject) => {
if(this.googleAds) {
resolve();
return;
}
const script = document.createElement('script');
let ready = false;
script.type = 'text/javascript';
script.src = src;
script.async = true;
script.onerror = e => {
console.log('adswizz inject error: ', JSON.stringify(e));
reject(e, script);
};
script.onload = script.onreadystatechange = () => {
if (!ready && (!this.readyState || this.readyState == 'complete')) {
ready = true;
resolve('src');
}
};
document.body.appendChild(script);
});
}
getVastAd() {
if (!this.promise) {
// get the GDPR information from the iframe (added in iframe-helper)
const sEncodedGDPR = this.getURLParameter('cmp');
let sConsent = '?aw_0_req.gdpr=false';
// if attribute exists, decode and parse to object, then update consent
if(sEncodedGDPR) {
const sGDPR = atob(sEncodedGDPR);
this.GDPR = JSON.parse(sGDPR);
if(this.GDPR.gdprApplies) {
sConsent = '?aw_0_req.gdpr=' + this.GDPR.gdprApplies;
}
}
this.googleAds = this.vastAdTag.includes('pubads.g.doubleclick.net');
// load first AdsWizz script
const src = location.protocol + '//synchrobox.adswizz.com/register2.php';
this.promise = this.loadScript(src + sConsent)
.then(() => {
// load second AdsWizz script
let src2 = location.protocol + '//cdn.adswizz.com/adswizz/js/SynchroClient2.js';
return this.loadScript(src2 + sConsent);
})
.then(() => {
//update vast URL with extra info once Adswizz script has loaded, request Ad
return new Promise((resolve, reject) => {
if(this.googleAds) {
//CODE FOR GOOLE AD MANAGER: overwrite url + description_url params
let sURL = new URL('http:'+this.vastAdTag);
let sQueryString = sURL.search;
let URLParams = new URLSearchParams(sQueryString);
// new value of "id" is set to "101"
URLParams.set('url', document.referrer);
URLParams.set('description_url', location.href);
// change the search property of the main url
sURL.search = URLParams.toString();
// the new url string
sURL = sURL.toString();
this.vastAdTag = sURL.slice(5);
console.log('loading Google Ad Tag: ', this.vastAdTag);
} else {
// normally wrap below in try/catch (promise will catch errors here)
this.vastAdTag=com_adswizz_synchro_decorateUrl(this.vastAdTag);
// add consent data if present
let sCMP = '&aw_0_req.gdpr=false';
if(this.GDPR.consentData) {
sCMP = '&aw_0_req.gdpr=' + this.GDPR.gdprApplies +
'&aw_0_req.userConsent=' + this.GDPR.consentData;
}
this.vastAdTag += sCMP;
// finally add cache buster
}
const cb = '&cb=' + (Math.round(new Date().getTime() / 1000));
this.vastAdTag += cb;
const sAdTagWithProtocol = location.protocol + this.vastAdTag;
return fetchWithErrorHandling(sAdTagWithProtocol, {
})
.then(response => response.text())
.then(stringToXML)
.then(xVastDoc => {
// get array of ads from Vast XML
const aAds = xVastDoc.getElementsByTagName('Ad');
if(!aAds.length) {
this.fireError(xVastDoc)
throw new Error("No Demand");
}
// getFirstAd will get object with details of first Ad in response
this.ads = this.getFirstAd(aAds);
return resolve(this.ads);
})
.catch(error => {
reject();
console.log('Get VAST Ad Error: '+ error);
});
})
});
}
return this.promise;
}
//append new trackingUrls of event type to oAds object
//takes tracking Node and gets & stores all events
addTrackingUrls(oAd, xEvents, aEventTypes) {
const aTrackingEvents = xEvents.getElementsByTagName('Tracking')
for (let i = 0; i < aEventTypes.length; i += 1) {
let sEventType = aEventTypes[i]
let aThisEventUrls = getContentOfNodesWithAttrValue(aTrackingEvents, 'event', sEventType)
for (let j = 0; j < aThisEventUrls.length; j += 1) {
if(!oAd.media[0].trackingUrls[sEventType]) {
oAd.media[0].trackingUrls[sEventType] = new Array();
}
oAd.media[0].trackingUrls[sEventType].push(aThisEventUrls[j]);
}
}
}
// in event of no Ad, fire error
fireError(xVastDoc) {
const xError = xVastDoc.getElementsByTagName('Error')[0]
if(xError.textContent) {
const img = new Image()
img.src = xError.textContent
}
}
// parse ads from the VAST repsonse, returning Ad object, populated with relevant data
getFirstAd(aAds) {
// get first Ad from VAST - WHEN FETCHING MORE THAN ONE AD, LOOK FOR SEQ=2 here
let xAd = getNodesWithAttrValue(aAds, 'sequence', '1')[0];
if(!xAd) {
xAd = aAds[0];
}
// next find both the linear creative and the companion
const xMediaCreative = getCreative(xAd)
// collect all useful nodes from the Ad:
const oNodes = {
xDuration: xMediaCreative.getElementsByTagName('Duration')[0],
xMediaFile: xMediaCreative.getElementsByTagName('MediaFile')[0],
xPricing: xAd.getElementsByTagName('Pricing')[0],
xAdTitle: xAd.getElementsByTagName('AdTitle')[0],
xAdvertiser: xAd.getElementsByTagName('Advertiser')[0],
xGoogleClickThrough: xAd.getElementsByTagName('ClickThrough')[0],
}
// Look for companion (optional) for more info
const xCompanion = xAd.getElementsByTagName('Companion')[0]
if(xCompanion) {
oNodes.xStaticResource = xCompanion.getElementsByTagName('StaticResource')[0]
oNodes.xAltText = xCompanion.getElementsByTagName('AltText')[0]
oNodes.xClickThrough = xCompanion.getElementsByTagName('CompanionClickThrough')[0]
}
const sAdvertiser = oNodes.xAdvertiser ? oNodes.xAdvertiser.textContent : 'Link';
const sClickThrough = oNodes.xClickThrough
? oNodes.xClickThrough.textContent
: oNodes.xGoogleClickThrough
? oNodes.xGoogleClickThrough.textContent
: ('https://' + sAdvertiser);
//loop over object to make sure nodes are populated:
// for (let key in oNodes) {
// if (!oNodes[key]) {
// //TODO: add error handling here
// console.log('VAST node ', key);
// }
// }
const oAd = {
id: xMediaCreative.getAttribute('id'),
campaign_id: xMediaCreative.getAttribute('id'),
campaign_name : sAdvertiser,
media: [{
bitrate: parseInt(oNodes.xMediaFile.getAttribute('bitrate')) || 0,
content_type: oNodes.xMediaFile.getAttribute('type'),
delivery: oNodes.xMediaFile.getAttribute('delivery'),
duration: this.getAdDuration(oNodes.xDuration) || 0,
logo: oNodes.xStaticResource ? oNodes.xStaticResource.textContent : '',
height: xCompanion ? parseInt(xCompanion.getAttribute('height')) : 0,
width: xCompanion ? parseInt(xCompanion.getAttribute('width')) : 0,
title: oNodes.xAltText ? oNodes.xAltText.textContent : 'no title',
pricing: {
currency: oNodes.xPricing ? oNodes.xPricing.getAttribute('currency') : 0,
model: oNodes.xPricing ? oNodes.xPricing.getAttribute('model') : 'CPM',
cost: oNodes.xPricing ? parseInt(oNodes.xPricing.textContent) : 0,
},
trackingUrls: {},
promo_link: sClickThrough,
url: oNodes.xMediaFile.textContent,
}],
}
// get tracking nodes from vast
const xTrackingEvents = xMediaCreative.getElementsByTagName('TrackingEvents')[0]
// const xCompanionTrackingEvents = xCompanion.getElementsByTagName('TrackingEvents')[0]
// set the events we want to track
const aCreativeEvents = ['start', 'firstQuartile', 'midpoint', 'thirdQuartile', 'complete']
// const aCompanionEvents = ['creativeView']
// get and store all the tracking URLs for the above events on our oAd object
this.addTrackingUrls(oAd, xTrackingEvents, aCreativeEvents)
// this.addTrackingUrls(oAd, xCompanionTrackingEvents, aCompanionEvents)
// const aErrorUrls = getVASTEventURLs(xVastDoc, 'Error')
oAd.media[0].trackingUrls.impression = getVASTEventURLs(xAd, 'Impression')
// oAd.media[0].trackingUrls.click = getVASTEventURLs(xCompanion, 'CompanionClickTracking')
//TODO: remove log, console.log('VAST Ad Object Created: ', oAd)
return oAd;
}
getAdDuration(xDuration) {
// add error case
if (!xDuration.textContent) {
return null
}
let aTimeComponents = xDuration.textContent.split(':'); // Split it at the colons.
// Map the array of strings to an array of numbers
aTimeComponents = aTimeComponents.map(sTime => +sTime);
// return total, minutes are worth 60 seconds, hours are worth 60 minutes
const nTimeSeconds = aTimeComponents[0] * 60 * 60 + aTimeComponents[1] * 60 + aTimeComponents[2];
return nTimeSeconds * 1000;
}
removeAds() {
if (this.ads) {
this.adsHistory = { ...this.ads }
}
this.ads = null
}
isPlaying() {
return this.adsIsPlaying;
}
isPlayed() {
return this.adsIsPlayed;
}
setPlaying() {
this.adsIsPlaying = true;
}
setPlayed() {
this.adsIsPlayed = true;
this.adsIsPlaying = false;
}
}
export default AdsService;