adUnit/VideoAdUnit.js

/* eslint-disable promise/prefer-await-to-callbacks */
import {linearEvents} from '../tracker';
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,
  iconClick,
  iconView
} = linearEvents;

// eslint-disable-next-line id-match
export const _protected = Symbol('_protected');

/**
 * @class
 * @extends Emitter
 * @alias VideoAdUnit
 * @implements LinearEvents
 * @description This class provides shared logic among all the ad units.
 */
class VideoAdUnit extends Emitter {
 [_protected] = {
   finish: () => {
     this[_protected].finished = true;
     this[_protected].onFinishCallbacks.forEach((callback) => callback());

     this.emit(
       finish,
       {
         adUnit: this,
         type: finish
       });
   },
   finished: false,
   onErrorCallbacks: [],
   onFinishCallbacks: [],
   started: false,
   throwIfCalled: () => {
     throw new Error('VideoAdUnit method must be implemented on child class');
   },
   throwIfFinished: () => {
     if (this.isFinished()) {
       throw new Error('VideoAdUnit is finished');
     }
   }
 };

   /** Ad unit type */
   type=null;

  /** If an error occurs it will contain the reference to the error otherwise it will be bull */
  error = null;

  /** If an error occurs it will contain the Vast Error code of the error */
  errorCode = null;

  /**
   * Creates a {@link VideoAdUnit}.
   *
   * @param {VastChain} vastChain - The {@link VastChain} with all the {@link VastResponse}
   * @param {VideoAdContainer} videoAdContainer - container instance to place the ad
   * @param {Object} [options] - Options Map. The allowed properties are:
   * @param {Console} [options.logger] - Optional logger instance. Must comply to the [Console interface]{@link https://developer.mozilla.org/es/docs/Web/API/Console}.
   * Defaults to `window.console`
   * @param {boolean} [options.viewability] - if true it will pause the ad whenever is not visible for the viewer.
   * Defaults to `false`
   * @param {boolean} [options.responsive] - if true it will resize the ad unit whenever the ad container changes sizes
   * Defaults to `false`
   */
  constructor (vastChain, videoAdContainer, {viewability = false, responsive = false, logger = console} = {}) {
    super(logger);

    const {
      onFinishCallbacks
    } = this[_protected];

    /** 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 null if there are no icons.*/
    this.icons = retrieveIcons(vastChain);

    onFinishCallbacks.push(preventManualProgress(this.videoAdContainer.videoElement));

    if (this.icons) {
      const {
        drawIcons,
        hasPendingIconRedraws,
        removeIcons
      } = addIcons(this.icons, {
        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[_protected].drawIcons = drawIcons;
      this[_protected].removeIcons = removeIcons;
      this[_protected].hasPendingIconRedraws = hasPendingIconRedraws;

      onFinishCallbacks.push(removeIcons);
    }

    if (viewability) {
      this.once(start, () => {
        const unsubscribe = onElementVisibilityChange(this.videoAdContainer.element, (visible) => {
          if (this.isFinished()) {
            return;
          }

          if (visible) {
            this.resume();
          } else {
            this.pause();
          }
        });

        onFinishCallbacks.push(unsubscribe);
      });
    }

    if (responsive) {
      this.once(start, () => {
        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 prevSize = this[_protected].size;
          const height = element.clientHeight;
          const width = element.clientWidth;

          if (height !== prevSize.height || width !== prevSize.width) {
            this.resize(width, height, viewmode(width, height));
          }
        });

        onFinishCallbacks.push(unsubscribe);
      });
    }
  }

  /*
   * 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();
  }

  /**
   * Sets the volume of the ad unit.
   *
   * @throws if ad unit is not started.
   * @throws if ad unit is finished.
   *
   * @param {number} volume - must be a value between 0 and 1;
   */
  // eslint-disable-next-line no-unused-vars
  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 {number} - 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 {number} - 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 {number} - 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 {Function} 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 {Function} 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 {boolean} - true if the ad unit is finished and false otherwise
   */
  isFinished () {
    return this[_protected].finished;
  }

  /**
   * @returns {boolean} - 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) {
    this[_protected].size = {
      height,
      viewmode: mode,
      width
    };

    if (this.isStarted() && !this.isFinished() && this.icons) {
      await this[_protected].removeIcons();
      await this[_protected].drawIcons();
    }
  }
}

export default VideoAdUnit;