video-ad-sdk
Version:
VAST/VPAID SDK that allows video ads to be played on top of any player
439 lines (438 loc) • 16.2 kB
JavaScript
var _a;
import { linearEvents, ErrorCode, isVastErrorCode } from '../tracker';
import { acceptInvitation, adCollapse } from '../tracker/nonLinearEvents';
import { getClickThrough } from '../vastSelectors';
import { volumeChanged, adProgress } from './adUnitEvents';
import { loadCreative } from './helpers/vpaid/loadCreative';
import { adLoaded, adStarted, adStopped, adPlaying, adPaused, startAd, stopAd, resumeAd, pauseAd, skipAd, setAdVolume, getAdVolume, getAdDuration, resizeAd, adSizeChange, adError, adVideoComplete, adSkipped, EVENTS, adVolumeChange, adImpression, adVideoStart, adVideoFirstQuartile, adVideoMidpoint, adVideoThirdQuartile, adUserAcceptInvitation, adUserMinimize, adUserClose, adDurationChange, adRemainingTimeChange, adClickThru, getAdIcons, getAdRemainingTime } from './helpers/vpaid/api';
import { waitFor } from './helpers/vpaid/waitFor';
import { callAndWait } from './helpers/vpaid/callAndWait';
import { handshake } from './helpers/vpaid/handshake';
import { initAd } from './helpers/vpaid/initAd';
import { AdUnitError } from './helpers/adUnitError';
import { VideoAdUnit, _protected } from './VideoAdUnit';
const { complete, mute, unmute, skip, start, firstQuartile, pause, resume, impression, midpoint, thirdQuartile, clickThrough, error: errorEvent, closeLinear, creativeView } = linearEvents;
// NOTE some ads only allow one handler per event and we need to subscribe to the adLoaded to know the creative is loaded.
const VPAID_EVENTS = EVENTS.filter((event) => event !== adLoaded);
const DRAW_ICONS_TIMEOUT = 500;
const WAIT_STOPPED_TIMEOUT = 3000;
const _private = Symbol('_private');
const vpaidGeneralError = (payload) => {
const error = payload instanceof Error ? payload : new AdUnitError('VPAID general error');
if (!error.code || !isVastErrorCode(error.code)) {
error.code = ErrorCode.VPAID_ERROR;
}
return error;
};
/**
* This class provides everything necessary to run a Vpaid ad.
*/
export class VpaidAdUnit extends VideoAdUnit {
/**
* Creates a {@link VpaidAdUnit}.
*
* @param vastChain The {@link VastChain} with all the {@link VastResponse}
* @param videoAdContainer container instance to place the ad
* @param options Options Map
*/
constructor(vastChain, videoAdContainer, options = {}) {
super(vastChain, videoAdContainer, options);
this[_a] = {
evtHandler: {
[adClickThru]: (url, _id, playerHandles) => {
if (playerHandles) {
this[_private].handleClickThrough(url);
}
this.emit(clickThrough, {
adUnit: this,
type: clickThrough
});
},
[adDurationChange]: () => {
this.emit(adProgress, {
adUnit: this,
type: adProgress
});
},
[adError]: (payload) => {
this.error = vpaidGeneralError(payload);
this.errorCode = this.error.code;
this[_protected].onErrorCallbacks.forEach((callback) => callback(this.error, {
adUnit: this,
vastChain: this.vastChain
}));
this[_protected].finish();
this.emit(errorEvent, {
adUnit: this,
type: errorEvent
});
},
[adImpression]: () => {
// NOTE: some ads forget to trigger the adVideoStart event. :(
if (!this[_private].videoStart) {
this[_private].handleVpaidEvent(adVideoStart);
}
this.emit(impression, {
adUnit: this,
type: impression
});
},
[adPaused]: () => {
this[_private].paused = true;
this.emit(pause, {
adUnit: this,
type: pause
});
},
[adPlaying]: () => {
this[_private].paused = false;
this.emit(resume, {
adUnit: this,
type: resume
});
},
[adRemainingTimeChange]: () => {
this.emit(adProgress, {
adUnit: this,
type: adProgress
});
},
[adSkipped]: () => {
this.cancel();
this.emit(skip, {
adUnit: this,
type: skip
});
},
[adStarted]: () => {
this.emit(creativeView, {
adUnit: this,
type: creativeView
});
},
[adStopped]: () => {
this.emit(adStopped, {
adUnit: this,
type: adStopped
});
this[_protected].finish();
},
[adUserAcceptInvitation]: () => {
this.emit(acceptInvitation, {
adUnit: this,
type: acceptInvitation
});
},
[adUserClose]: () => {
this.emit(closeLinear, {
adUnit: this,
type: closeLinear
});
this[_protected].finish();
},
[adUserMinimize]: () => {
this.emit(adCollapse, {
adUnit: this,
type: adCollapse
});
},
[adVideoComplete]: () => {
this.emit(complete, {
adUnit: this,
type: complete
});
},
[adVideoFirstQuartile]: () => {
this.emit(firstQuartile, {
adUnit: this,
type: firstQuartile
});
},
[adVideoMidpoint]: () => {
this.emit(midpoint, {
adUnit: this,
type: midpoint
});
},
[adVideoStart]: () => {
if (!this[_private].videoStart) {
this[_private].videoStart = true;
this[_private].paused = false;
this.emit(start, {
adUnit: this,
type: start
});
}
},
[adVideoThirdQuartile]: () => {
this.emit(thirdQuartile, {
adUnit: this,
type: thirdQuartile
});
},
[adVolumeChange]: () => {
const volume = this.getVolume();
this.emit(volumeChanged, {
adUnit: this,
type: volumeChanged
});
if (volume === 0 && !this[_private].muted) {
this[_private].muted = true;
this.emit(mute, {
adUnit: this,
type: mute
});
}
if (volume > 0 && this[_private].muted) {
this[_private].muted = false;
this.emit(unmute, {
adUnit: this,
type: unmute
});
}
}
},
handleVpaidEvent: (event, ...args) => {
const handler = this[_private].evtHandler[event];
if (handler) {
handler(...args);
}
this.emit(event, {
adUnit: this,
type: event
});
},
handleClickThrough: (url) => {
if (this.paused() && this.pauseOnAdClick) {
this.resume();
return;
}
const inlineAd = this.vastChain[0].ad;
const clickThroughUrl = typeof url === 'string' && url.length > 0
? url
: inlineAd && getClickThrough(inlineAd);
if (this.pauseOnAdClick) {
this.pause();
}
if (clickThroughUrl) {
window.open(clickThroughUrl, '_blank');
}
},
getIcons: () => {
var _b;
if ((_b = this.creativeAd) === null || _b === void 0 ? void 0 : _b[getAdIcons]) {
try {
if (!this.creativeAd[getAdIcons]()) {
delete this.icons;
}
}
catch (_c) {
delete this.icons;
}
}
},
drawIcons: async () => {
var _b, _c, _d, _e;
if (this.isFinished()) {
return;
}
await ((_c = (_b = this[_protected]).drawIcons) === null || _c === void 0 ? void 0 : _c.call(_b));
if (((_e = (_d = this[_protected]).hasPendingIconRedraws) === null || _e === void 0 ? void 0 : _e.call(_d)) && !this.isFinished()) {
setTimeout(this[_private].drawIcons, DRAW_ICONS_TIMEOUT);
}
},
muted: false,
paused: true
};
/** Ad unit type. Will be `VPAID` for VpaidAdUnit */
this.type = 'VPAID';
this[_private].loadCreativePromise = loadCreative(vastChain, videoAdContainer);
}
/**
* Starts the ad unit.
*
* @throws if called twice.
* @throws if ad unit is finished.
*/
async start() {
this[_protected].throwIfFinished();
if (this.isStarted()) {
throw new Error('VpaidAdUnit already started');
}
try {
this.creativeAd = (await this[_private]
.loadCreativePromise);
const adLoadedPromise = waitFor(this.creativeAd, adLoaded);
for (const creativeEvent of VPAID_EVENTS) {
this.creativeAd.subscribe(this[_private].handleVpaidEvent.bind(this, creativeEvent), creativeEvent);
}
this[_private].getIcons();
handshake(this.creativeAd, '2.0');
initAd(this.creativeAd, this.videoAdContainer, this.vastChain);
await adLoadedPromise;
// if the ad timed out while trying to load the videoAdContainer will be destroyed
if (this.videoAdContainer.isDestroyed()) {
return;
}
try {
const { videoElement } = this.videoAdContainer;
if (videoElement.muted) {
this[_private].muted = true;
this.setVolume(0);
}
else {
this.setVolume(videoElement.volume);
}
await callAndWait(this.creativeAd, startAd, adStarted);
if (this.icons) {
await this[_private].drawIcons();
}
this[_protected].started = true;
}
catch (_b) {
this.cancel();
}
}
catch (error) {
this[_private].handleVpaidEvent(adError, error);
throw error;
}
}
/**
* Resumes a previously paused ad unit.
*
* @throws if ad unit is not started.
* @throws if ad unit is finished.
*/
resume() {
var _b;
(_b = this.creativeAd) === null || _b === void 0 ? void 0 : _b[resumeAd]();
}
/**
* Pauses the ad unit.
*
* @throws if ad unit is not started.
* @throws if ad unit is finished.
*/
pause() {
var _b;
(_b = this.creativeAd) === null || _b === void 0 ? void 0 : _b[pauseAd]();
}
/**
* Skip the ad unit.
*
* @throws if ad unit is not started.
* @throws if ad unit is finished.
*/
skip() {
var _b;
(_b = this.creativeAd) === null || _b === void 0 ? void 0 : _b[skipAd]();
}
/**
* Returns true if the ad is paused and false otherwise
*/
paused() {
return this.isFinished() || this[_private].paused;
}
/**
* Sets the volume of the ad unit.
*
* @throws if ad unit is not started.
* @throws if ad unit is finished.
*
* @param volume must be a value between 0 and 1;
*/
setVolume(volume) {
var _b;
(_b = this.creativeAd) === null || _b === void 0 ? void 0 : _b[setAdVolume](volume);
}
/**
* Gets the volume of the ad unit.
*
* @throws if ad unit is not started.
* @throws if ad unit is finished.
*
* @returns the volume of the ad unit.
*/
getVolume() {
if (!this.creativeAd) {
return 0;
}
return this.creativeAd[getAdVolume]();
}
/**
* Cancels the ad unit.
*
* @throws if ad unit is finished.
*/
async cancel() {
var _b;
this[_protected].throwIfFinished();
try {
const adStoppedPromise = this.creativeAd &&
waitFor(this.creativeAd, adStopped, WAIT_STOPPED_TIMEOUT);
(_b = this.creativeAd) === null || _b === void 0 ? void 0 : _b[stopAd]();
await adStoppedPromise;
}
catch (_c) {
this[_protected].finish();
}
}
/**
* Returns the duration of the ad Creative or 0 if there is no creative.
*
* Note: if the user has engaged with the ad, the duration becomes unknown and it will return 0;
*
* @returns the duration of the ad unit.
*/
duration() {
if (!this.creativeAd) {
return 0;
}
const duration = this.creativeAd[getAdDuration]();
if (duration < 0) {
return 0;
}
return duration;
}
/**
* Returns the current time of the ad Creative or 0 if there is no creative.
*
* Note: if the user has engaged with the ad, the currentTime becomes unknown and it will return 0;
*
* @returns the current time of the ad unit.
*/
currentTime() {
if (!this.creativeAd) {
return 0;
}
const remainingTime = this.creativeAd[getAdRemainingTime]();
if (remainingTime < 0) {
return 0;
}
return this.duration() - remainingTime;
}
/**
* This method resizes the ad unit to fit the available space in the passed {@link VideoAdContainer}
*
* @throws if ad unit is not started.
* @throws if ad unit is finished.
*
* @returns Promise that resolves once the unit was resized
*/
async resize(width, height, viewmode) {
await super.resize(width, height, viewmode);
if (!this.creativeAd) {
return;
}
if (this.isStarted() && !this.isFinished()) {
const slot = this.videoAdContainer.slotElement;
if (slot) {
slot.style.height = `${height}px`;
slot.style.width = `${width}px`;
}
}
return callAndWait(this.creativeAd, resizeAd, adSizeChange, width, height, viewmode);
}
}
_a = _private;