MailOnline/VPAIDHTML5Client

View on GitHub
demo/VPAIDAdLinear.js

Summary

Maintainability
C
1 day
Test Coverage
'use strict';


(function() {

/**
 * VPAIDAdLinear
 *
 * @class
 */
var VPAIDAdLinear = function VPAIDAdLinear() {
    this._slot = null;
    this._videoSlot = null;

    this._subscribers = {};

    this._attributes = {
        companions: '',
        desiredBitrate: 256,
        duration: 30,
        expanded: false,
        icons: '',
        linear: true,
        remainingTime: 10,
        skippableState: false,
        viewMode: 'normal',
        width: 0,
        volume: 1.0,
        size: {
            height: 0,
            width: 0
        }
    };

    //open interactive panel -> AdExpandedChange, AdInteraction
    //when close panel -> AdExpandedChange, AdInteraction

    this._quartileEvents = [
        {event: 'AdVideoStart', position: 0},
        {event: 'AdVideoFirstQuartile', position: 25},
        {event: 'AdVideoMidpoint', position: 50},
        {event: 'AdSkippableStateChange', position: 65, hook: $enableSkippable.bind(this)},
        {event: 'AdVideoThirdQuartile', position: 75},
        {event: 'AdVideoComplete', position: 100}
    ];

    this._lastQuartilePosition = this._quartileEvents[0];

    this._parameters = {};
};

/**
 * handshakeVersion
 *
 * @param {string} playerVPAIDVersion
 * @return {string} adUnit VPAID version format 'major.minor.patch' minimum '1.0'
 */
VPAIDAdLinear.prototype.handshakeVersion = function (playerVPAIDVersion) {
    return '2.0';
};

/**
 * initAd
 *
 * @param {number} width
 * @param {number} height
 * @param {string} viewMode
 * @param {number} desiredBitrate
 * @param {object} creativeData
 * @param {object} environmentVars
 */
VPAIDAdLinear.prototype.initAd = function initAd(width, height, viewMode, desiredBitrate, creativeData, environmentVars) {
    this._attributes.size.width = width;
    this._attributes.size.height = height;
    this._attributes.viewMode = viewMode;
    this._attributes.desiredBitrate = desiredBitrate;

    this._slot = environmentVars.slot;
    this._videoSlot = environmentVars.videoSlot;
    this._style = _addCssLink('ad.css');

    try {
        this._parameters = JSON.parse(creativeData.AdParameters);
    } catch (e) {
        return $throwError('failed to parse creativeData.AdParameters, mandatory for this ad');
    }

    $setVideoAd.call(this);
    this._videoSlot.addEventListener('timeupdate', $onVideoUpdated.bind(this), false);
    this._videoSlot.addEventListener('ended', $onVideoEnded.bind(this), false);

    $trigger.call(this, 'AdLoaded');
};

/**
 * startAd
 *
 */
VPAIDAdLinear.prototype.startAd = function() {
    this._videoSlot.play();

    this._ui = {};
    this._ui.buy = _createAndAppend(this._slot, 'div', 'vpaidAdLinear');
    this._ui.banner = _createAndAppend(this._slot, 'div', 'banner');
    this._ui.xBtn = _createAndAppend(this._slot, 'button', 'close');
    this._ui.interact = _createAndAppend(this._slot, 'div', 'interact');

    this._ui.buy.addEventListener('click', $onClickThru.bind(this), false);
    this._ui.banner.addEventListener('click', $toggleExpand.bind(this, true), false);
    this._ui.xBtn.addEventListener('click', $toggleExpand.bind(this, false), false);

    $trigger.call(this, 'AdStarted');
};

/**
 * stopAd
 *
 */
VPAIDAdLinear.prototype.stopAd = function() {
    if (this._destroyed) return;

    $removeAll.call(this);
    $trigger.call(this, 'AdStopped');
};

/**
 * skipAd
 *
 */
VPAIDAdLinear.prototype.skipAd = function() {
    if (this._destroyed) return;
    if (!this._attributes.skippableState) return;

    $removeAll.call(this);
    $trigger.call(this, 'AdSkipped');
    $trigger.call(this, 'AdStopped');
};

/**
 * resizeAd
 *
 */
VPAIDAdLinear.prototype.resizeAd = function() {
    //TODO
};

/**
 * pauseAd
 *
 */
VPAIDAdLinear.prototype.pauseAd = function() {
    this._videoSlot.pause();
    $trigger.call(this, 'AdPaused');
};

/**
 * resumeAd
 *
 */
VPAIDAdLinear.prototype.resumeAd = function() {
    this._videoSlot.play();
    $trigger.call(this, 'AdPlaying');
};

/**
 * expandAd
 *
 */
VPAIDAdLinear.prototype.expandAd = function() {
    //TODO
};

/**
 * collapseAd
 *
 */
VPAIDAdLinear.prototype.collapseAd = function() {
    //TODO
};


/**
 * subscribe
 *
 * @param {function} handler
 * @param {string} event
 * @param {object} context
 */
VPAIDAdLinear.prototype.subscribe = function subscribe(handler, event, context) {
    if (!this._subscribers[event]) {
        this._subscribers[event] = [];
    }
    this._subscribers[event].push({callback: handler, context: context});
};


/**
 * unsubscribe
 *
 * @param {function} handler
 * @param {string} event
 */
VPAIDAdLinear.prototype.unsubscribe = function unsubscribe(handler, event) {
    var eventSubscribers = this._subscribers[event];
    if (!Array.isArray(eventSubscribers)) return;

    this._subscribers[event] = eventSubscribers.filter(function (subscriber) {
        return handler !== subscriber;
    });
};

/**
 * getAdLinear
 *
 * @return {boolean}
 */
VPAIDAdLinear.prototype.getAdLinear = function getAdLinear() {
    return this._attributes.linear;
};

/**
 * getAdWidth
 *
 * @return {number} pixel's size of the ad, is equal to or less than the values passed in resizeAd/initAd
 */
VPAIDAdLinear.prototype.getAdWidth = function getAdWidth() {
    return this._attributes.width;
};

/**
 * getAdHeight
 *
 * @return {number} pixel's size of the ad, is equal to or less than the values passed in resizeAd/initAd
 */
VPAIDAdLinear.prototype.getAdHeight = function getAdHeight() {
    return this._attributes.height;
};

/**
 * getAdExpanded
 *
 * @return {boolean}
 */
VPAIDAdLinear.prototype.getAdExpanded = function getAdExpanded() {
    return this._attributes.expanded;
};

/**
 * getAdSkippableState - if the ad is in the position to be able to skip
 *
 * @return {boolean}
 */
VPAIDAdLinear.prototype.getAdSkippableState = function getAdSkippableState() {
    return this._attributes.skippableState;
};

/**
 * getAdRemainingTime
 *
 * @return {number} seconds, if not implemented will return -1, or -2 if the time is unknown (user is engaged with the ad)
 */
VPAIDAdLinear.prototype.getAdRemainingTime = function getAdRemainingTime() {};

/**
 * getAdDuration
 *
 * @return {number} seconds, if not implemented will return -1, or -2 if the time is unknown (user is engaged with the ad)
 */
VPAIDAdLinear.prototype.getAdDuration = function getAdDuration() {
    return this._attributes.duration;
};

/**
 * getAdVolume
 *
 * @return {number} between 0 and 1, if is not implemented will return -1
 */
VPAIDAdLinear.prototype.getAdVolume = function getAdVolume() {
    return this._attributes.volume;
};

/**
 * getAdCompanions - companions are banners outside the video player to reinforce the ad
 *
 * @return {string} VAST 3.0 formart string for <CompanionAds>
 */
VPAIDAdLinear.prototype.getAdCompanions = function getAdCompanions() {
    return this._attributes.companions;
};

/**
 * getAdIcons
 *
 * @return {boolean} if true videoplayer may hide is own icons to not duplicate
 */
VPAIDAdLinear.prototype.getAdIcons = function getAdIcons() {
    return this._attributes.icons;
};

/**
 * setAdVolume
 *
 * @param {number} volume  between 0 and 1
 */
VPAIDAdLinear.prototype.setAdVolume = function(volume) {
    if (volume < 0 || volume > 1) return $throwError('volume is not valid');

    this._videoSlot.volume = volume;
    this._attributes.volume = volume;
};

function $enableSkippable() {
    this._attributes.skippableState = true;
}

function $onVideoUpdated() {
    if (this._destroyed) return;

    var videoSlot = this._videoSlot;
    var percentPlayed = _mapNumber(0, videoSlot.duration, 0, 100, videoSlot.currentTime);
    var last = this._lastQuartilePosition;

    if (percentPlayed < last.position) return;

    if (last.hook) last.hook();

    $trigger.call(this, last.event);

    var quartile = this._quartileEvents;
    this._lastQuartilePosition = quartile[ quartile.indexOf(last) + 1 ];
}

function $onVideoEnded() {
    if (this._destroyed) return;

    $removeAll.call(this);
    $trigger.call(this, 'AdStopped');
}

function $onClickThru() {
    var clickThru = this._parameters.clickThru || {
        url: 'http://www.dailymail.com',
        trackID: 123,
        playerHandles: false
    };

    $trigger.call(this, 'AdClickThru', [clickThru.url, clickThru.trackID, clickThru.playerHandles]);

    if (!clickThru.playerHandles) {
        window.open(clickThru.url, '_blank');
    }
}

function $removeAll() {
    this._destroyed = true;
    this._videoSlot.src = '';
    this._style.parentElement.removeChild(this._style);
    this._slot.innerHTML = '';
    this._ui = null;
}

function $throwError(msg) {
    $trigger.call(this, 'AdError', msg);
}

function $trigger(event, msg) {
    var subscribers = this._subscribers[event] || [];
    subscribers.forEach(function(handlers) {
        handlers.callback.apply(handlers.context, msg);
    });
}

function $setVideoAd() {
    var videoSlot = this._videoSlot;

    if (!videoSlot) {
        return $throwError.call(this, 'no video');
    }
    _setSize(videoSlot, this._attributes.size);

    if (!_setSupportedVideo(videoSlot, this._parameters.videos || [])) {
        return $throwError.call(this, 'no supported video found');
    }
}

function $toggleExpand(toExpand) {
    $toggleUI.call(this, toExpand);
    $togglePlay.call(this, toExpand);

    this._attributes.expandAd = toExpand;
    this._attributes.remainingTime = toExpand ? -2 : -1;

    $trigger.call(this, 'AdExpandedChange');
    $trigger.call(this, 'AdDurationChange');
}

function $togglePlay(toPlay) {
    if (toPlay) {
        this._videoSlot.pause();
    } else {
        this._videoSlot.play();
    }
}

function $toggleUI(show) {
    this._ui.interact.style.display = getDisplay();
    this._ui.xBtn.style.display = getDisplay();

    function getDisplay() {
        return show ? 'block' : 'none';
    }
}

function _setSize(el, size) {
    el.setAttribute('width', size.width);
    el.setAttribute('height', size.height);
    el.style.width = size.width + 'px';
    el.style.height = size.height + 'px';
}

function _setSupportedVideo(videoEl, videos) {
    var supportedVideos = videos.filter(function (video) {
        return videoEl.canPlayType(video.mimetype);
    });

    if (supportedVideos.length === 0) return false;

    videoEl.setAttribute('src', supportedVideos[0].url);

    return true;
}

function _createAndAppend(parent, tagName, className) {
    var el = document.createElement(tagName || 'div');
    el.className = className || '';
    parent.appendChild(el);
    return el;
}

function _addCssLink() {
    var css = document.createElement('link');
    css.rel = 'stylesheet';
    css.href = 'ad.css';
    parent.document.body.appendChild(css);
    return css;
}

function _normNumber(start, end, value) {
    return (value - start) / (end - start);
}

function _mapNumber(fromStart, fromEnd, toStart, toEnd, value) {
    return toStart + (toEnd - toStart) * _normNumber(fromStart, fromEnd, value);
}

window.getVPAIDAd = function() {
    return new VPAIDAdLinear();
};

})();