Source: lib/ads/hls_interstitial_parser.js

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


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

goog.require('shaka.ads.SvtaInterstitialParser');
goog.require('shaka.ads.Utils');
goog.require('shaka.net.NetworkingEngine');
goog.require('shaka.util.NumberUtils');
goog.require('shaka.util.URL');


/**
 * Interprets HLS interstitial metadata without playing media. The static
 * methods are pure; an instance holds the state of the current asset (the
 * playback session ID and the Date Ranges already seen).
 */
shaka.ads.HlsInterstitialParser = class {
  constructor() {
    /** @private {?string} */
    this.sessionId_ = null;
    /** @private {!Set<string>} */
    this.metadataIds_ = new Set();
  }

  /**
   * Converts a metadata frame into what the interstitial manager must
   * schedule or update. Insertion (HLS interstitials) and measurement (SVTA)
   * can be enabled independently.
   *
   * @param {shaka.extern.HLSMetadata} metadata
   * @param {boolean} isLive
   * @param {!shaka.net.NetworkingEngine} networkingEngine
   * @param {boolean} insertion Whether HLS interstitials are enabled.
   * @param {boolean} measurement Whether SVTA signaling is enabled.
   * @return {!shaka.ads.HlsInterstitialParser.Result}
   */
  parse(metadata, isLive, networkingEngine, insertion, measurement) {
    const Parser = shaka.ads.HlsInterstitialParser;
    const SvtaParser = shaka.ads.SvtaInterstitialParser;
    /** @type {!shaka.ads.HlsInterstitialParser.Result} */
    const result = {
      interstitials: [],
      deferred: [],
      playoutLimitUpdate: null,
      preloadHint: null,
    };
    const svta = measurement && SvtaParser.isValidMetadata(metadata);
    const parseSvta = () => {
      const parsed = SvtaParser.parseMetadata(metadata, networkingEngine);
      result.interstitials = parsed.interstitials;
      result.deferred = parsed.deferred;
      return result;
    };
    if (metadata.type == 'com.apple.hls.preload') {
      // RFC 8216bis Appendix F: the target's resources may be resolved from
      // the start of this Date Range on.
      const targetId = Parser.getMetadataValue(metadata, 'X-TARGET-ID');
      if (insertion && targetId) {
        result.preloadHint = {targetId, startTime: metadata.startTime};
      }
      return result;
    }
    if (!insertion || !Parser.isInterstitialMetadata(metadata)) {
      // Embedded measurement is independent of disabling insertions.
      return svta ? parseSvta() : result;
    }
    const id = Parser.getMetadataValue(metadata, 'ID');
    // A subsequent EXT-X-DATERANGE with the same ID augments the existing
    // Date Range with additional attributes (RFC 8216bis Section 4.4.5.1).
    const limit = parseFloat(
        Parser.getMetadataValue(metadata, 'X-PLAYOUT-LIMIT') || '');
    if (id && Number.isFinite(limit)) {
      result.playoutLimitUpdate = {id, playoutLimit: limit};
    }
    if (id && this.metadataIds_.has(id)) {
      return result;
    }
    const parsed = Parser.parseMetadata(metadata, isLive);
    if (!parsed.interstitials.length && !parsed.assetList) {
      // Native HLS groups date-range attributes under its legacy metadata
      // type, including SVTA slots already embedded in the stream.
      return svta ? parseSvta() : result;
    }
    if (id) {
      this.metadataIds_.add(id);
    }
    if (!this.sessionId_) {
      this.sessionId_ = window.crypto.randomUUID();
    }
    const sessionId = this.sessionId_;
    const signaling = svta ? SvtaParser.decodeMetadata(metadata) : null;
    for (const interstitial of parsed.interstitials) {
      interstitial.uri = Parser.getUriWithParams(
          interstitial.uri || '', sessionId);
      SvtaParser.decorateInterstitial(interstitial, signaling);
    }
    result.interstitials = parsed.interstitials;
    if (parsed.assetList) {
      result.deferred.push(Parser.createDeferredInterstitial(
          parsed.assetList, networkingEngine, sessionId));
    }
    return result;
  }

  /**
   * @param {shaka.extern.HLSMetadata} metadata
   * @return {boolean}
   */
  static isInterstitialMetadata(metadata) {
    const Parser = shaka.ads.HlsInterstitialParser;
    const hasAsset = Parser.getMetadataValue(metadata, 'X-ASSET-URI') != null ||
        Parser.getMetadataValue(metadata, 'X-ASSET-LIST') != null;
    return metadata.type == 'com.apple.hls.interstitial' ||
        metadata.type == 'com.apple.quicktime.HLS' ||
        (hasAsset &&
        shaka.ads.SvtaInterstitialParser.isValidMetadata(metadata));
  }

  /**
   * @param {shaka.extern.HLSMetadata} hlsMetadata
   * @param {boolean} isLive
   * @return {!shaka.ads.HlsInterstitialParser.ParsedMetadata}
   */
  static parseMetadata(hlsMetadata, isLive) {
    const Parser = shaka.ads.HlsInterstitialParser;
    const NumberUtils = shaka.util.NumberUtils;

    const interstitialsAd = [];
    const result = {interstitials: interstitialsAd, assetList: null};
    if (Parser.getMetadataValue(hlsMetadata, 'X-OVERLAY-ID') != null) {
      result.interstitials = Parser.parseOverlay_(hlsMetadata);
      return result;
    }
    if (!hlsMetadata) {
      return result;
    }
    const assetUri = Parser.getMetadataValue(hlsMetadata, 'X-ASSET-URI');
    const assetList = Parser.getMetadataValue(hlsMetadata, 'X-ASSET-LIST');
    if (!assetUri && !assetList) {
      return result;
    }
    const id = Parser.getMetadataValue(hlsMetadata, 'ID');
    const {startTime, endTime} = Parser.getInterstitialTimes_(hlsMetadata, id);
    const restrict = Parser.getMetadataValue(hlsMetadata, 'X-RESTRICT');
    let isSkippable = true;
    let canJump = true;
    if (restrict != null) {
      isSkippable = !restrict.includes('SKIP');
      canJump = !restrict.includes('JUMP');
    }
    let skipOffset = isSkippable ? 0 : null;
    const skipControlOffset =
        Parser.getMetadataValue(hlsMetadata, 'X-SKIP-CONTROL-OFFSET');
    if (skipControlOffset != null) {
      skipOffset = parseFloat(skipControlOffset);
      if (isNaN(skipOffset)) {
        skipOffset = isSkippable ? 0 : null;
      }
    }
    let skipFor = null;
    const skipControlDuration =
        Parser.getMetadataValue(hlsMetadata, 'X-SKIP-CONTROL-DURATION');
    if (skipControlDuration != null) {
      skipFor = parseFloat(skipControlDuration);
      if (isNaN(skipFor)) {
        skipFor = null;
      }
    }
    let resumeOffset = null;
    const resume = Parser.getMetadataValue(hlsMetadata, 'X-RESUME-OFFSET');
    if (resume != null) {
      resumeOffset = parseFloat(resume);
      if (isNaN(resumeOffset)) {
        resumeOffset = null;
      }
    }
    if (resumeOffset != null && resumeOffset != 0 && endTime &&
        endTime != Infinity &&
        NumberUtils.isFloatEqual(startTime + resumeOffset, endTime)) {
      resumeOffset = null;
    }
    let playoutLimit = null;
    const playout = Parser.getMetadataValue(hlsMetadata, 'X-PLAYOUT-LIMIT');
    if (playout != null) {
      playoutLimit = parseFloat(playout);
      if (isNaN(playoutLimit)) {
        playoutLimit = null;
      }
    }
    const {once, pre, post} = Parser.parseCue_(hlsMetadata);
    let timelineRange = false;
    const timelineOccupies =
        Parser.getMetadataValue(hlsMetadata, 'X-TIMELINE-OCCUPIES');
    if (timelineOccupies != null) {
      timelineRange = timelineOccupies.includes('RANGE');
    } else if (resume == null && isLive) {
      timelineRange = !pre && !post;
    }
    if (assetUri != null) {
      if (!assetUri) {
        return result;
      }
      interstitialsAd.push(shaka.ads.Utils.createInterstitial({
        id,
        startTime,
        endTime,
        uri: assetUri,
        isSkippable,
        skipOffset,
        skipFor,
        canJump,
        resumeOffset,
        playoutLimit,
        once,
        pre,
        post,
        timelineRange,
      }));
    } else if (assetList != null) {
      if (!assetList) {
        return result;
      }
      /** @type {shaka.ads.HlsInterstitialParser.AssetListDescriptor} */
      const descriptor = {
        id,
        groupId: null,
        startTime,
        endTime,
        assetListUri: assetList,
        isSkippable,
        skipOffset,
        skipFor,
        canJump,
        resumeOffset,
        playoutLimit,
        once,
        pre,
        post,
        timelineRange,
        resolving: false,
        resolved: false,
      };
      result.assetList = descriptor;
    }
    return result;
  }

  /**
   * @param {shaka.extern.HLSMetadata} hlsMetadata
   * @return {!Array<shaka.extern.AdInterstitial>}
   * @private
   */
  static parseOverlay_(hlsMetadata) {
    const Parser = shaka.ads.HlsInterstitialParser;
    const interstitialsAd = [];
    if (!hlsMetadata) {
      return interstitialsAd;
    }
    const uri = Parser.getMetadataValue(hlsMetadata, 'X-ASSET-URI');
    if (!uri) {
      return interstitialsAd;
    }
    const id = Parser.getMetadataValue(hlsMetadata, 'X-OVERLAY-ID');
    const {startTime, endTime} = Parser.getInterstitialTimes_(hlsMetadata, id);
    const {once, pre, post} = Parser.parseCue_(hlsMetadata);
    const mimeType = Parser.getMetadataValue(hlsMetadata, 'X-ASSET-MIMETYPE');
    const loop = Parser.getMetadataValue(hlsMetadata, 'X-LOOP') == 'YES';
    let z = 1;
    const depth = Parser.getMetadataValue(hlsMetadata, 'X-DEPTH');
    if (depth != null) {
      z = parseFloat(depth);
      if (isNaN(z)) {
        z = 1;
      }
    }
    const background = Parser.getMetadataValue(hlsMetadata, 'X-BACKGROUND');

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

    const viewportValue = Parser.getMetadataValue(hlsMetadata, 'X-VIEWPORT');
    if (viewportValue != null) {
      const size = viewportValue.split('x');
      if (size.length != 2) {
        return interstitialsAd;
      }
      viewport.x = parseFloat(size[0]);
      viewport.y = parseFloat(size[1]);
    }

    /** @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 overlayPosition =
        Parser.getMetadataValue(hlsMetadata, 'X-OVERLAY-POSITION');
    if (overlayPosition != null) {
      const position = overlayPosition.split('x');
      if (position.length != 2) {
        return interstitialsAd;
      }
      overlay.topLeft.x = parseFloat(position[0]);
      overlay.topLeft.y = parseFloat(position[1]);
    }

    const overlaySize = Parser.getMetadataValue(hlsMetadata, 'X-OVERLAY-SIZE');
    if (overlaySize != null) {
      const size = overlaySize.split('x');
      if (size.length != 2) {
        return interstitialsAd;
      }
      overlay.size.x = parseFloat(size[0]);
      overlay.size.y = parseFloat(size[1]);
    }

    /** @type {?shaka.extern.AdPositionInfo} */
    let currentVideo = null;
    const squeezeCurrent =
        Parser.getMetadataValue(hlsMetadata, 'X-SQUEEZECURRENT');
    if (squeezeCurrent != null) {
      let percentage = parseFloat(squeezeCurrent);
      if (isNaN(percentage)) {
        percentage = 1;
      }
      currentVideo = {
        viewport: {
          x: 1920,
          y: 1080,
        },
        topLeft: {
          x: 0,
          y: 0,
        },
        size: {
          x: 1920 * percentage,
          y: 1080 * percentage,
        },
      };
      const squeezeCurrentPosition =
          Parser.getMetadataValue(hlsMetadata, 'X-SQUEEZECURRENT-POSITION');
      if (squeezeCurrentPosition != null) {
        const position = squeezeCurrentPosition.split('x');
        if (position.length != 2) {
          return interstitialsAd;
        }
        currentVideo.topLeft.x = parseFloat(position[0]);
        currentVideo.topLeft.y = parseFloat(position[1]);
      }
    }

    interstitialsAd.push(shaka.ads.Utils.createInterstitial({
      id,
      startTime,
      endTime,
      uri,
      mimeType,
      once,
      pre,
      post,
      timelineRange: true,
      loop,
      overlay,
      displayOnBackground: z == -1,
      currentVideo,
      background,
    }));
    return interstitialsAd;
  }

  /**
   * Parses a fetched asset list without performing requests. URI resolution
   * uses the response URI, including redirects. Assets skipped on a live
   * start retain their original indexes and offsets.
   *
   * @param {!shaka.ads.HlsInterstitialParser.AssetsList} data
   * @param {!shaka.ads.HlsInterstitialParser.AssetListDescriptor} descriptor
   * @param {string} responseUri
   * @param {number=} startOffset
   * @return {!Array<shaka.extern.AdInterstitial>}
   */
  static parseAssetList(data, descriptor, responseUri, startOffset = 0) {
    if (!data || !Array.isArray(data['ASSETS'])) {
      throw new Error('Invalid HLS asset list');
    }
    const control = data['SKIP-CONTROL'] || {};
    const skipOffset = Number.isFinite(control['OFFSET']) ?
        control['OFFSET'] : descriptor.skipOffset;
    const skipFor = Number.isFinite(control['DURATION']) ?
        control['DURATION'] : descriptor.skipFor;
    const SvtaParser = shaka.ads.SvtaInterstitialParser;
    const podEnvelope = SvtaParser.parseEnvelope(
        data[SvtaParser.KEY], 'pod');
    const pod = podEnvelope ?
        /** @type {!shaka.extern.AdCreativeSignaling.Pod} */
        (podEnvelope.payload[0]) : null;
    const slotEnvelopes = data['ASSETS'].map((asset) =>
      asset && SvtaParser.parseEnvelope(
          asset[SvtaParser.KEY], 'slot'));
    const podSize = slotEnvelopes.filter((slot) => slot != null).length;
    const interstitials = [];
    let position = 0;
    let cumulativeDuration = 0;
    for (let i = 0; i < data['ASSETS'].length; i++) {
      const asset = data['ASSETS'][i];
      if (slotEnvelopes[i]) {
        position++;
      }
      if (!asset || typeof asset['URI'] != 'string' ||
          asset['DURATION'] < 0) {
        throw new Error('Invalid HLS asset description');
      }
      const duration = parseFloat(asset['DURATION']) || 0;
      const assetOffset = cumulativeDuration;
      cumulativeDuration += duration;
      if (!asset['URI'] || (duration > 0 &&
          cumulativeDuration <= startOffset)) {
        continue;
      }
      const interstitial = shaka.ads.Utils.createInterstitial({
        id: descriptor.id + '_shaka_asset_' + i,
        groupId: descriptor.id,
        startTime: descriptor.startTime,
        endTime: descriptor.endTime,
        uri: shaka.util.URL.resolve(responseUri, asset['URI']),
        isSkippable: descriptor.isSkippable,
        skipOffset,
        skipFor,
        canJump: descriptor.canJump,
        resumeOffset: descriptor.resumeOffset,
        playoutLimit: descriptor.playoutLimit,
        once: descriptor.once,
        pre: descriptor.pre,
        post: descriptor.post,
        timelineRange: descriptor.timelineRange,
      });
      SvtaParser.decorateInterstitial(interstitial, slotEnvelopes[i], pod);
      if (interstitial.adCreativeSignaling) {
        interstitial.podSize = podSize;
        interstitial.podPosition = position;
      }
      // Playback offsets are execution context, separate from the slot's
      // offset in its pod. The manager uses this to start within an asset.
      if (startOffset > assetOffset) {
        interstitial.startOffset = startOffset - assetOffset;
      }
      interstitials.push(interstitial);
    }
    return interstitials;
  }

  /**
   * Creates a deferred resolver for an HLS asset list. Parsing remains pure;
   * only invoking resolve performs the request. The common manager controls
   * when to invoke it and can abort it without knowing the resource format.
   *
   * @param {!shaka.ads.HlsInterstitialParser.AssetListDescriptor} descriptor
   * @param {!shaka.net.NetworkingEngine} networkingEngine
   * @param {string} sessionId
   * @return {!shaka.extern.DeferredInterstitial}
   */
  static createDeferredInterstitial(descriptor, networkingEngine, sessionId) {
    const Parser = shaka.ads.HlsInterstitialParser;
    // The resource holds all the descriptor attributes, including updates
    // of its playout limit received while it is unresolved.
    return shaka.ads.Utils.createDeferredInterstitial(descriptor,
        networkingEngine,
        (offset) => Parser.getUriWithParams(
            descriptor.assetListUri, sessionId, offset),
        (data, uri, offset, resource) => {
          const interstitials = Parser.parseAssetList(
              /** @type {!shaka.ads.HlsInterstitialParser.AssetsList} */ (data),
              /** @type {?} */ (resource), uri, offset);
          for (const interstitial of interstitials) {
            interstitial.uri = Parser.getUriWithParams(
                interstitial.uri || '', sessionId);
          }
          return interstitials;
        }, {
          type: shaka.net.NetworkingEngine.AdvancedRequestType
              .INTERSTITIAL_ASSET_LIST,
        });
  }

  /**
   * @param {string} uri
   * @param {string} sessionId
   * @param {number=} offset
   * @return {string}
   */
  static getUriWithParams(uri, sessionId, offset = 0) {
    if (uri.startsWith('data:')) {
      return uri;
    }
    const params = new Map();
    params.set('_HLS_primary_id', sessionId);
    if (offset > 0) {
      params.set('_HLS_start_offset',
          String(Math.round(offset * 1000) / 1000));
    }
    return shaka.util.URL.appendParams(uri, params);
  }

  /**
   * Returns the string data of the HLS metadata frame with the given key, or
   * null if there is no such frame.
   *
   * @param {shaka.extern.HLSMetadata} hlsMetadata
   * @param {string} key
   * @return {?string}
   */
  static getMetadataValue(hlsMetadata, key) {
    const frame = hlsMetadata.values.find((v) => v.key == key);
    return frame ? /** @type {string} */ (frame.data) : null;
  }

  /**
   * Computes the start/end times of an interstitial from its HLS metadata. When
   * the Date Range has no ID, the times are floored to a tenth of a second.
   *
   * @param {shaka.extern.HLSMetadata} hlsMetadata
   * @param {?string} id
   * @return {{startTime: number, endTime: ?number}}
   * @private
   */
  static getInterstitialTimes_(hlsMetadata, id) {
    const startTime = id == null ?
        Math.floor(hlsMetadata.startTime * 10) / 10 :
        hlsMetadata.startTime;
    let endTime = hlsMetadata.endTime;
    if (hlsMetadata.endTime && hlsMetadata.endTime != Infinity &&
        typeof(hlsMetadata.endTime) == 'number') {
      endTime = id == null ?
          Math.floor(hlsMetadata.endTime * 10) / 10 :
          hlsMetadata.endTime;
    }
    return {startTime, endTime};
  }

  /**
   * Parses the CUE attribute (ONCE/PRE/POST) from HLS metadata. X-CUE was
   * the provisional name and is supported as a fallback for compatibility.
   *
   * @param {shaka.extern.HLSMetadata} hlsMetadata
   * @return {{once: boolean, pre: boolean, post: boolean}}
   * @private
   */
  static parseCue_(hlsMetadata) {
    const Parser = shaka.ads.HlsInterstitialParser;
    const cue = Parser.getMetadataValue(hlsMetadata, 'CUE') ||
        Parser.getMetadataValue(hlsMetadata, 'X-CUE');
    return {
      once: cue != null && cue.includes('ONCE'),
      pre: cue != null && cue.includes('PRE'),
      post: cue != null && cue.includes('POST'),
    };
  }
};


/**
 * @typedef {{
 *   interstitials: !Array<shaka.extern.AdInterstitial>,
 *   assetList: ?shaka.ads.HlsInterstitialParser.AssetListDescriptor,
 * }}
 */
shaka.ads.HlsInterstitialParser.ParsedMetadata;


/**
 * @typedef {{
 *   interstitials: !Array<shaka.extern.AdInterstitial>,
 *   deferred: !Array<!shaka.extern.DeferredInterstitial>,
 *   playoutLimitUpdate: ?{id: string, playoutLimit: number},
 *   preloadHint: ?{targetId: string, startTime: number},
 * }}
 *
 * @property {!Array<shaka.extern.AdInterstitial>} interstitials
 *   Interstitials ready to be scheduled.
 * @property {!Array<!shaka.extern.DeferredInterstitial>} deferred
 *   Resources (asset lists, remote pods) to be resolved before scheduling.
 * @property {?{id: string, playoutLimit: number}} playoutLimitUpdate
 *   A playout limit for an interstitial or resource that may already exist.
 * @property {?{targetId: string, startTime: number}} preloadHint
 *   The time from which the target's resources may be resolved.
 */
shaka.ads.HlsInterstitialParser.Result;


/**
 * Holds the parsed metadata of an HLS X-ASSET-LIST interstitial whose
 * resolution has been deferred until playback approaches it.
 *
 * @typedef {{
 *   id: ?string,
 *   groupId: ?string,
 *   startTime: number,
 *   endTime: ?number,
 *   assetListUri: string,
 *   isSkippable: boolean,
 *   skipOffset: ?number,
 *   skipFor: ?number,
 *   canJump: boolean,
 *   resumeOffset: ?number,
 *   playoutLimit: ?number,
 *   once: boolean,
 *   pre: boolean,
 *   post: boolean,
 *   timelineRange: boolean,
 *   resolutionTimeOffset: (number|undefined),
 *   resolving: boolean,
 *   resolved: boolean,
 * }}
 *
 * @property {?string} id
 * @property {?string} groupId
 * @property {number} startTime
 * @property {?number} endTime
 * @property {string} assetListUri
 * @property {boolean} isSkippable
 * @property {?number} skipOffset
 * @property {?number} skipFor
 * @property {boolean} canJump
 * @property {?number} resumeOffset
 * @property {?number} playoutLimit
 * @property {boolean} once
 * @property {boolean} pre
 * @property {boolean} post
 * @property {boolean} timelineRange
 * @property {(number|undefined)} resolutionTimeOffset
 *   The offset in seconds before startTime at which the asset list may be
 *   resolved. Undefined or 0 means use the interstitialPreloadAheadTime
 *   default.
 * @property {boolean} resolving
 *   Whether a deferred resolution request is already in flight.
 * @property {boolean} resolved
 *   Whether the asset list has already been resolved. Retained (only for live
 *   streams that allow starting mid-interstitial) so that seeking back into the
 *   break can re-request the asset list with an updated _HLS_start_offset.
 */
shaka.ads.HlsInterstitialParser.AssetListDescriptor;


/* eslint-disable @stylistic/max-len */
/**
 * @typedef {{
 *   ASSETS: !Array<shaka.ads.HlsInterstitialParser.Asset>,
 *   SKIP-CONTROL: (shaka.ads.HlsInterstitialParser.SkipControl|undefined),
 *   X-AD-CREATIVE-SIGNALING: (shaka.extern.AdCreativeSignaling.CarriageEnvelope|undefined),
 * }}
 *
 * @property {!Array<shaka.ads.HlsInterstitialParser.Asset>} ASSETS
 * @property {?shaka.ads.HlsInterstitialParser.SkipControl} SKIP-CONTROL
 * @property {?shaka.extern.AdCreativeSignaling.CarriageEnvelope} X-AD-CREATIVE-SIGNALING
 */
shaka.ads.HlsInterstitialParser.AssetsList;
/* eslint-enable @stylistic/max-len */


/* eslint-disable @stylistic/max-len */
/**
 * @typedef {{
 *   URI: string,
 *   DURATION: number,
 *   X-AD-CREATIVE-SIGNALING: (shaka.extern.AdCreativeSignaling.CarriageEnvelope|undefined),
 * }}
 *
 * @property {string} URI
 * @property {number} DURATION
 * @property {?shaka.extern.AdCreativeSignaling.CarriageEnvelope} X-AD-CREATIVE-SIGNALING
 */
shaka.ads.HlsInterstitialParser.Asset;
/* eslint-enable @stylistic/max-len */


/**
 * @typedef {{
 *   OFFSET: (number|undefined),
 *   DURATION: (number|undefined),
 * }}
 *
 * @property {number} OFFSET
 * @property {number} DURATION
 */
shaka.ads.HlsInterstitialParser.SkipControl;