Source: lib/ads/dash_interstitial_parser.js

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


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

goog.require('goog.asserts');
goog.require('shaka.log');
goog.require('shaka.util.StringUtils');
goog.require('shaka.util.TXml');
goog.require('shaka.util.URL');


/**
 * A class responsible for parsing DASH alternative MPD and overlay event
 * regions into interstitials.
 */
shaka.ads.DashInterstitialParser = class {
  /**
   * Whether the given DASH region is an interstitial handled by this parser:
   * an alternative-MPD insert/replace event, or an overlay event.
   *
   * @param {shaka.extern.TimelineRegionInfo} region
   * @return {boolean}
   */
  static isInterstitialRegion(region) {
    if (shaka.ads.DashInterstitialParser.isAlternativeMpd_(region)) {
      return true;
    }
    const schemeIdUri = region.schemeIdUri;
    return (schemeIdUri == 'urn:mpeg:dash:event:2012' ||
        schemeIdUri == 'urn:scte:dash:scte214-events') &&
        !!region.eventNode &&
        !!shaka.util.TXml.findChild(region.eventNode, 'OverlayEvent');
  }

  /**
   * Parses a DASH interstitial region (alternative MPD or overlay event) into
   * an interstitial. Returns null if the region is not a valid interstitial.
   * Callers should gate this with isInterstitialRegion.
   *
   * @param {shaka.extern.TimelineRegionInfo} region
   * @return {?shaka.extern.AdInterstitial}
   */
  static parseRegion(region) {
    if (shaka.ads.DashInterstitialParser.isAlternativeMpd_(region)) {
      return shaka.ads.DashInterstitialParser.parseAlternativeMpd_(region);
    }
    return shaka.ads.DashInterstitialParser.parseOverlay_(region);
  }

  /**
   * @param {shaka.extern.TimelineRegionInfo} region
   * @return {boolean}
   * @private
   */
  static isAlternativeMpd_(region) {
    return region.schemeIdUri ==
            'urn:mpeg:dash:event:alternativeMPD:insert:2025' ||
        region.schemeIdUri ==
            'urn:mpeg:dash:event:alternativeMPD:replace:2025';
  }

  /**
   * @param {shaka.extern.TimelineRegionInfo} region
   * @return {?shaka.extern.AdInterstitial}
   * @private
   */
  static parseAlternativeMpd_(region) {
    const TXml = shaka.util.TXml;
    const isReplace =
        region.schemeIdUri == 'urn:mpeg:dash:event:alternativeMPD:replace:2025';
    const isInsert =
        region.schemeIdUri == 'urn:mpeg:dash:event:alternativeMPD:insert:2025';
    if (!isReplace && !isInsert) {
      shaka.log.warning('Unsupported alternative media presentation', region);
      return null;
    }

    const startTime = region.startTime;
    let endTime = region.endTime;
    let playoutLimit = null;
    let resumeOffset = 0;
    let interstitialUri;
    let canJump = true;
    let skipOffset = null;
    let resolutionTimeOffset = null;
    for (const node of region.eventNode.children) {
      if (node.tagName == 'AlternativeMPD') {
        const uri = node.attributes['uri'];
        if (uri) {
          interstitialUri = uri;
          break;
        }
      } else if (node.tagName == 'InsertPresentation' ||
          node.tagName == 'ReplacePresentation') {
        const uri = node.attributes['uri'] || node.attributes['url'];
        if (uri) {
          interstitialUri = shaka.util.StringUtils.htmlUnescape(uri);
          const unscaledMaxDuration =
              TXml.parseAttr(node, 'maxDuration', TXml.parseInt);
          if (unscaledMaxDuration) {
            playoutLimit = unscaledMaxDuration / region.timescale;
          }
          const unscaledReturnOffset =
              TXml.parseAttr(node, 'returnOffset', TXml.parseInt);
          if (unscaledReturnOffset) {
            resumeOffset = unscaledReturnOffset / region.timescale;
          }
          if (isReplace && resumeOffset) {
            endTime = startTime + resumeOffset;
          }
          const noJump = TXml.parseAttr(node, 'noJump', TXml.parseInt);
          if (noJump) {
            canJump = false;
          }
          const skipAfter =
              TXml.parseAttr(node, 'skipAfter', TXml.parseDuration);
          if (typeof skipAfter == 'number') {
            skipOffset = skipAfter;
          }
          const unscaledResolutionOffset = TXml.parseAttr(
              node, 'earliestResolutionTimeOffset', TXml.parseFloat);
          if (unscaledResolutionOffset) {
            resolutionTimeOffset = unscaledResolutionOffset / region.timescale;
          }
          break;
        }
      }
    }
    if (!interstitialUri) {
      shaka.log.warning('Unsupported alternative media presentation', region);
      return null;
    }

    interstitialUri =
        shaka.util.URL.applyUrlParams(interstitialUri, region.urlParams);

    /** @type {!shaka.extern.AdInterstitial} */
    const interstitial = {
      id: region.id,
      groupId: null,
      startTime,
      endTime,
      uri: interstitialUri,
      mimeType: null,
      isSkippable: skipOffset != null,
      skipOffset: skipOffset,
      skipFor: null,
      canJump: canJump,
      resumeOffset: isInsert ? resumeOffset : null,
      playoutLimit,
      once: false,
      pre: false,
      post: false,
      timelineRange: isReplace && !isInsert,
      loop: false,
      overlay: null,
      displayOnBackground: false,
      currentVideo: null,
      background: null,
      clickThroughUrl: null,
      tracking: null,
    };
    if (resolutionTimeOffset != null) {
      interstitial.resolutionTimeOffset = resolutionTimeOffset;
    }
    return interstitial;
  }

  /**
   * @param {shaka.extern.TimelineRegionInfo} region
   * @return {?shaka.extern.AdInterstitial}
   * @private
   */
  static parseOverlay_(region) {
    const TXml = shaka.util.TXml;

    goog.asserts.assert(region.eventNode, 'Need a region eventNode');
    const overlayEvent = TXml.findChild(region.eventNode, 'OverlayEvent');
    const uri = overlayEvent.attributes['uri'];
    const mimeType = overlayEvent.attributes['mimeType'];
    const loop = overlayEvent.attributes['loop'] == 'true';
    const z = TXml.parseAttr(overlayEvent, 'z', TXml.parseInt);
    if (!uri || z == 0) {
      shaka.log.warning('Unsupported OverlayEvent', region);
      return null;
    }

    let background = null;
    const backgroundElement = TXml.findChild(overlayEvent, 'Background');
    if (backgroundElement) {
      const backgroundUri = backgroundElement.attributes['uri'];
      if (backgroundUri) {
        background = `center / contain no-repeat url('${backgroundUri}')`;
      } else {
        background = TXml.getContents(backgroundElement);
      }
    }

    const viewport = {
      x: 1920,
      y: 1080,
    };

    const viewportElement = TXml.findChild(overlayEvent, 'Viewport');
    if (viewportElement) {
      const viewportX = TXml.parseAttr(viewportElement, 'x', TXml.parseInt);
      if (viewportX == null) {
        shaka.log.warning('Unsupported OverlayEvent', region);
        return null;
      }
      const viewportY = TXml.parseAttr(viewportElement, 'y', TXml.parseInt);
      if (viewportY == null) {
        shaka.log.warning('Unsupported OverlayEvent', region);
        return null;
      }
      viewport.x = viewportX;
      viewport.y = viewportY;
    }

    /** @type {!shaka.extern.AdPositionInfo} */
    const overlay = {
      viewport: {
        x: viewport.x,
        y: viewport.y,
      },
      topLeft: {
        x: 0,
        y: 0,
      },
      size: {
        x: viewport.x,
        y: viewport.y,
      },
    };

    const overlayElement = TXml.findChild(overlayEvent, 'Overlay');
    if (viewportElement && overlayElement) {
      const topLeft = TXml.findChild(overlayElement, 'TopLeft');
      const size = TXml.findChild(overlayElement, 'Size');
      if (topLeft && size) {
        const parsed =
            shaka.ads.DashInterstitialParser.parseTopLeftSize_(topLeft, size);
        if (!parsed) {
          shaka.log.warning('Unsupported OverlayEvent', region);
          return null;
        }
        overlay.topLeft = parsed.topLeft;
        overlay.size = parsed.size;
      }
    }
    let currentVideo = null;
    const squeezeElement = TXml.findChild(overlayEvent, 'Squeeze');
    if (viewportElement && squeezeElement) {
      const topLeft = TXml.findChild(squeezeElement, 'TopLeft');
      const size = TXml.findChild(squeezeElement, 'Size');
      if (topLeft && size) {
        const parsed =
            shaka.ads.DashInterstitialParser.parseTopLeftSize_(topLeft, size);
        if (!parsed) {
          shaka.log.warning('Unsupported OverlayEvent', region);
          return null;
        }
        currentVideo = {
          viewport: {
            x: viewport.x,
            y: viewport.y,
          },
          topLeft: parsed.topLeft,
          size: parsed.size,
        };
      }
    }

    /** @type {!shaka.extern.AdInterstitial} */
    const interstitial = {
      id: region.id,
      groupId: null,
      startTime: region.startTime,
      endTime: region.endTime,
      uri,
      mimeType,
      isSkippable: false,
      skipOffset: null,
      skipFor: null,
      canJump: true,
      resumeOffset: null,
      playoutLimit: null,
      once: false,
      pre: false,
      post: false,
      timelineRange: true,
      loop,
      overlay,
      displayOnBackground: z == -1,
      currentVideo,
      background,
      clickThroughUrl: null,
      tracking: null,
    };
    return interstitial;
  }

  /**
   * Parses the integer x/y attributes of TopLeft and Size nodes (used by DASH
   * overlay events). Returns null if any coordinate is missing or invalid.
   *
   * @param {!shaka.extern.xml.Node} topLeft
   * @param {!shaka.extern.xml.Node} size
   * @return {?{topLeft: {x: number, y: number}, size: {x: number, y: number}}}
   * @private
   */
  static parseTopLeftSize_(topLeft, size) {
    const TXml = shaka.util.TXml;
    const topLeftX = TXml.parseAttr(topLeft, 'x', TXml.parseInt);
    const topLeftY = TXml.parseAttr(topLeft, 'y', TXml.parseInt);
    const sizeX = TXml.parseAttr(size, 'x', TXml.parseInt);
    const sizeY = TXml.parseAttr(size, 'y', TXml.parseInt);
    if (topLeftX == null || topLeftY == null ||
        sizeX == null || sizeY == null) {
      return null;
    }
    return {
      topLeft: {x: topLeftX, y: topLeftY},
      size: {x: sizeX, y: sizeY},
    };
  }
};