video-ad-sdk
Version:
VAST/VPAID SDK that allows video ads to be played on top of any player
315 lines (314 loc) • 11.7 kB
JavaScript
var _a, _b;
import { linearEvents } from '../tracker';
import { getViewable } from '../vastSelectors';
import { finish } from './adUnitEvents';
import { onElementVisibilityChange, onElementResize } from './helpers/dom/elementObservers';
import { preventManualProgress } from './helpers/dom/preventManualProgress';
import { Emitter } from './helpers/Emitter';
import { retrieveIcons } from './helpers/icons/retrieveIcons';
import { addIcons } from './helpers/icons/addIcons';
import { viewmode } from './helpers/vpaid/viewmode';
import { safeCallback } from './helpers/safeCallback';
const { start, viewable, notViewable, viewUndetermined, iconClick, iconView } = linearEvents;
const VIEWABLE_IMPRESSION_TIMEOUT = 2000;
const _private = Symbol('_private');
export const _protected = Symbol('_protected');
/**
* This class provides shared logic among all the ad units.
*/
export class VideoAdUnit extends Emitter {
/**
* Creates a {@link VideoAdUnit}.
*
* @param vastChain The {@link VastChain} with all the {@link VastResponse}
* @param videoAdContainer container instance to place the ad
* @param options Options Map. The allowed properties are:
*/
constructor(vastChain, videoAdContainer, { viewability = false, responsive = false, logger = console, pauseOnAdClick = true } = {}) {
super(logger);
this[_a] = {
addIcons: () => {
if (!this.icons) {
return;
}
const { drawIcons, hasPendingIconRedraws, removeIcons } = addIcons(this.icons, {
logger: this.logger,
onIconClick: (icon) => this.emit(iconClick, {
adUnit: this,
data: icon,
type: iconClick
}),
onIconView: (icon) => this.emit(iconView, {
adUnit: this,
data: icon,
type: iconView
}),
videoAdContainer: this.videoAdContainer
});
this[_protected].drawIcons = drawIcons;
this[_protected].removeIcons = removeIcons;
this[_protected].hasPendingIconRedraws = hasPendingIconRedraws;
this[_protected].onFinishCallbacks.push(removeIcons);
},
setupViewableImpression: () => {
let timeoutId;
const unsubscribe = onElementVisibilityChange(this.videoAdContainer.element, (visible) => {
if (this.isFinished() || this[_protected].viewable) {
return;
}
if (typeof visible !== 'boolean') {
this[_private].handleViewableImpression(viewUndetermined);
return;
}
if (visible) {
timeoutId = window.setTimeout(this[_private].handleViewableImpression, VIEWABLE_IMPRESSION_TIMEOUT, viewable);
}
else {
clearTimeout(timeoutId);
}
}, { viewabilityOffset: 0.5 });
this[_protected].onFinishCallbacks.push(() => {
unsubscribe();
clearTimeout(timeoutId);
if (!this[_protected].viewable) {
this[_private].handleViewableImpression(notViewable);
}
});
},
handleViewableImpression: (event) => {
this[_protected].viewable = Boolean(event);
this.emit(event, {
adUnit: this,
type: event
});
},
setupViewability: () => {
const unsubscribe = onElementVisibilityChange(this.videoAdContainer.element, (visible) => {
if (this.isFinished()) {
return;
}
if (typeof visible === 'boolean') {
if (visible) {
this.resume();
}
else {
this.pause();
}
}
});
this[_protected].onFinishCallbacks.push(unsubscribe);
},
setupResponsive: () => {
const { element } = this.videoAdContainer;
this[_protected].size = {
height: element.clientHeight,
viewmode: viewmode(element.clientWidth, element.clientHeight),
width: element.clientWidth
};
const unsubscribe = onElementResize(element, () => {
if (this.isFinished()) {
return;
}
const previousSize = this[_protected].size;
const height = element.clientHeight;
const width = element.clientWidth;
if (height !== (previousSize === null || previousSize === void 0 ? void 0 : previousSize.height) || width !== (previousSize === null || previousSize === void 0 ? void 0 : previousSize.width)) {
this.resize(width, height, viewmode(width, height));
}
});
this[_protected].onFinishCallbacks.push(unsubscribe);
}
};
this[_b] = {
finished: false,
started: false,
viewable: false,
onErrorCallbacks: [],
onFinishCallbacks: [],
finish: () => {
if (!this.isFinished()) {
this[_protected].finished = true;
this[_protected].onFinishCallbacks.forEach((callback) => callback());
this.emit(finish, {
adUnit: this,
type: finish
});
}
},
throwIfCalled: () => {
throw new Error('VideoAdUnit method must be implemented on child class');
},
throwIfFinished: () => {
if (this.isFinished()) {
throw new Error('VideoAdUnit is finished');
}
}
};
/** Reference to the {@link VastChain} used to load the ad. */
this.vastChain = vastChain;
/** Reference to the {@link VideoAdContainer} that contains the ad. */
this.videoAdContainer = videoAdContainer;
/** Array of {@link VastIcon} definitions to display from the passed {@link VastChain} or undefined if there are no icons.*/
this.icons = retrieveIcons(vastChain);
this.pauseOnAdClick = pauseOnAdClick;
this[_protected].onFinishCallbacks.push(preventManualProgress(this.videoAdContainer.videoElement));
this[_private].addIcons();
const viewableImpression = vastChain.some(({ ad }) => ad && getViewable(ad));
if (viewableImpression) {
this.once(start, this[_private].setupViewableImpression);
}
if (viewability) {
this.once(start, this[_private].setupViewability);
}
if (responsive) {
this.once(start, this[_private].setupResponsive);
}
}
/*
* Starts the ad unit.
*
* @throws if called twice.
* @throws if ad unit is finished.
*/
start() {
this[_protected].throwIfCalled();
}
/**
* Resumes a previously paused ad unit.
*
* @throws if ad unit is not started.
* @throws if ad unit is finished.
*/
resume() {
this[_protected].throwIfCalled();
}
/**
* Pauses the ad unit.
*
* @throws if ad unit is not started.
* @throws if ad unit is finished.
*/
pause() {
this[_protected].throwIfCalled();
}
/**
* Skips the ad unit.
*
* @throws if ad unit is not started.
* @throws if ad unit is finished.
*/
skip() {
this[_protected].throwIfCalled();
}
/**
* 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) {
this[_protected].throwIfCalled();
}
/**
* 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() {
this[_protected].throwIfCalled();
}
/**
* Cancels the ad unit.
*
* @throws if ad unit is finished.
*/
cancel() {
this[_protected].throwIfCalled();
}
/**
* Returns the duration of the ad Creative or 0 if there is no creative.
*
* @returns the duration of the ad unit.
*/
duration() {
this[_protected].throwIfCalled();
}
/**
* Returns true if the ad is paused and false otherwise
*/
paused() {
this[_protected].throwIfCalled();
}
/**
* Returns the current time of the ad Creative or 0 if there is no creative.
*
* @returns the current time of the ad unit.
*/
currentTime() {
this[_protected].throwIfCalled();
}
/**
* Register a callback function that will be called whenever the ad finishes. No matter if it was finished because de ad ended, or cancelled or there was an error playing the ad.
*
* @throws if ad unit is finished.
*
* @param callback will be called once the ad unit finished
*/
onFinish(callback) {
if (typeof callback !== 'function') {
throw new TypeError('Expected a callback function');
}
this[_protected].onFinishCallbacks.push(safeCallback(callback, this.logger));
}
/**
* Register a callback function that will be called if there is an error while running the ad.
*
* @throws if ad unit is finished.
*
* @param callback will be called on ad unit error passing the Error instance and an object with the adUnit and the {@link VastChain}.
*/
onError(callback) {
if (typeof callback !== 'function') {
throw new TypeError('Expected a callback function');
}
this[_protected].onErrorCallbacks.push(safeCallback(callback, this.logger));
}
/**
* @returns true if the ad unit is finished and false otherwise
*/
isFinished() {
return this[_protected].finished;
}
/**
* @returns true if the ad unit has started and false otherwise
*/
isStarted() {
return this[_protected].started;
}
/**
* 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, mode) {
var _c, _d, _e, _f;
this[_protected].size = {
height,
viewmode: mode,
width
};
if (this.isStarted() && !this.isFinished() && this.icons) {
await ((_d = (_c = this[_protected]).removeIcons) === null || _d === void 0 ? void 0 : _d.call(_c));
await ((_f = (_e = this[_protected]).drawIcons) === null || _f === void 0 ? void 0 : _f.call(_e));
}
}
}
_a = _private, _b = _protected;