Source: lib/ads/interstitial_static_presentation.js

/*! @license
 * Shaka Player
 * Copyright 2016 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */


goog.provide('shaka.ads.InterstitialStaticPresentation');

goog.require('shaka.ads.LayoutRenderer');
goog.require('shaka.ads.Utils');
goog.require('shaka.util.EventManager');
goog.require('shaka.util.IReleasable');
goog.require('shaka.util.Timer');


/**
 * Presents the image or HTML document of a non-linear interstitial over the
 * primary content, and ends it when its window or its playout limit is over.
 *
 * @implements {shaka.util.IReleasable}
 */
shaka.ads.InterstitialStaticPresentation = class {
  /**
   * @param {shaka.extern.AdInterstitial} interstitial
   * @param {!HTMLElement} container The element the assets are drawn in.
   * @param {!HTMLMediaElement} video The primary video.
   * @param {function(string, Map<string, ?>=)} onEvent Sends an ad event for
   *   the interstitial.
   * @param {function()} onClick Called when an asset is clicked, if the
   *   interstitial has a click-through.
   * @param {function()} onEnd Called once the presentation has ended and its
   *   assets have been removed.
   */
  constructor(interstitial, container, video, onEvent, onClick, onEnd) {
    /** @private {shaka.extern.AdInterstitial} */
    this.interstitial_ = interstitial;

    /** @private {!HTMLElement} */
    this.container_ = container;

    /** @private {!HTMLMediaElement} */
    this.video_ = video;

    /** @private {function(string, Map<string, ?>=)} */
    this.onEvent_ = onEvent;

    /** @private {function()} */
    this.onClick_ = onClick;

    /** @private {function()} */
    this.onEnd_ = onEnd;

    /** @private {!Array<!HTMLElement>} */
    this.elements_ = [];

    /** @private {!shaka.util.EventManager} */
    this.eventManager_ = new shaka.util.EventManager();

    /** @private {?shaka.util.Timer} */
    this.timer_ = null;

    /** @private {boolean} */
    this.released_ = false;
  }

  /**
   * Draws the assets and starts watching for the end of the presentation.
   */
  start() {
    const Utils = shaka.ads.Utils;
    const LayoutRenderer = shaka.ads.LayoutRenderer;
    const interstitial = this.interstitial_;
    const video = this.video_;

    const layout = LayoutRenderer.fromInterstitial(interstitial);
    const isHtml = interstitial.mimeType == 'text/html';
    const element = /** @type {!(HTMLImageElement|HTMLIFrameElement)} */ (
      document.createElement(isHtml ? 'iframe' : 'img'));
    element.style.objectFit = 'contain';
    element.style.position = 'absolute';
    element.style.border = 'none';
    element.src = interstitial.uri;
    if (!isHtml) {
      this.eventManager_.listen(element, 'error', (e) => {
        this.end_(Utils.AD_ERROR, (new Map()).set('originalEvent', e));
      });
    }
    LayoutRenderer.placeAsset(element, layout.assets[0]);
    this.container_.appendChild(element);
    this.elements_.push(element);

    const startTime = Date.now();
    this.timer_ = new shaka.util.Timer(() => {
      // The playout limit is read on each tick, since it can be updated
      // while the interstitial is presented. Without an end time the timer
      // ticks once, exactly when the limit is reached.
      if (interstitial.playoutLimit &&
          (Date.now() - startTime) / 1000 >= interstitial.playoutLimit) {
        this.end_(Utils.AD_COMPLETE);
      } else if (interstitial.endTime &&
          video.currentTime > interstitial.endTime) {
        this.end_(Utils.AD_COMPLETE);
      } else if (video.currentTime < interstitial.startTime) {
        this.end_(Utils.AD_SKIPPED);
      }
    });
    if (interstitial.playoutLimit && !interstitial.endTime) {
      this.timer_.tickAfter(interstitial.playoutLimit);
    } else if (interstitial.endTime) {
      this.timer_.tickEvery(/* seconds= */ 0.025);
    }
    this.eventManager_.listen(video, 'seeked', () => {
      const currentTime = video.currentTime;
      if (currentTime < interstitial.startTime ||
          (interstitial.endTime && currentTime > interstitial.endTime)) {
        this.end_(Utils.AD_SKIPPED);
      }
    });
    if (interstitial.clickThroughUrl) {
      for (const asset of this.elements_) {
        this.eventManager_.listen(asset, 'click', () => this.onClick_());
      }
    }
  }

  /**
   * Removes the assets without sending any event.
   *
   * @override
   */
  release() {
    if (this.released_) {
      return;
    }
    this.released_ = true;
    this.timer_?.stop();
    this.timer_ = null;
    this.eventManager_.release();
    for (const element of this.elements_) {
      element.remove();
    }
    this.elements_ = [];
  }

  /**
   * @param {string} type The event that ends the presentation.
   * @param {Map<string, ?>=} data
   * @private
   */
  end_(type, data) {
    if (this.released_) {
      return;
    }
    this.onEvent_(type, data);
    this.release();
    this.onEnd_();
  }
};