Source: lib/util/scte35.js

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

goog.provide('shaka.util.Scte35');

goog.require('shaka.util.TXml');
goog.require('shaka.util.Uint8ArrayUtils');

/**
 * Recognizes SCTE-35 messages in the transports that can carry them, and
 * normalizes them onto the presentation timeline.  The payload itself is
 * passed through untouched for the application to decode.
 */
shaka.util.Scte35 = class {
  /**
   * @param {string} scheme
   * @return {boolean}
   */
  static isScheme(scheme) {
    return shaka.util.Scte35.SCHEMES_.includes(scheme.trim());
  }

  /**
   * @param {shaka.extern.TimelineRegionInfo} region
   * @return {?shaka.extern.Scte35Event}
   */
  static fromRegion(region) {
    if (!shaka.util.Scte35.isScheme(region.schemeIdUri)) {
      return null;
    }
    const event = {
      schemeIdUri: region.schemeIdUri,
      startTime: region.startTime,
      endTime: region.endTime,
      id: region.id,
      source: 'dash',
      kind: '',
      data: null,
      node: null,
    };
    if (region.eventNode) {
      // xml+bin wraps the section in <Binary>; plain xml has no binary form.
      event.data = shaka.util.Scte35.binaryPayload_(region.eventNode);
      if (!event.data) {
        event.node = region.eventNode;
      }
    }
    return event;
  }

  /**
   * @param {shaka.extern.EmsgInfo} emsg
   * @return {?shaka.extern.Scte35Event}
   */
  static fromEmsg(emsg) {
    if (!shaka.util.Scte35.isScheme(emsg.schemeIdUri)) {
      return null;
    }
    const event = {
      schemeIdUri: emsg.schemeIdUri,
      startTime: emsg.startTime,
      // 0xffffffff means the duration is unknown.
      endTime: emsg.eventDuration == 0xffffffff ?
          emsg.startTime : emsg.endTime,
      id: String(emsg.id),
      source: 'emsg',
      kind: '',
      data: null,
      node: null,
    };
    if (emsg.messageData && emsg.messageData.length) {
      if (emsg.schemeIdUri.trim().endsWith(':bin')) {
        event.data = emsg.messageData;
      } else {
        // A message we cannot even decode as text must not fail playback.
        let node = null;
        try {
          node = shaka.util.TXml.parseXml(emsg.messageData);
        } catch (error) {}
        if (node) {
          event.data = shaka.util.Scte35.binaryPayload_(node) ?? event.node;
        }
      }
    }
    return event;
  }

  /**
   * Finds the base64 <Binary> section, ignoring the producer's namespace
   * prefix and how deeply it nested the element.
   * @param {!shaka.extern.xml.Node} node
   * @return {?Uint8Array}
   * @private
   */
  static binaryPayload_(node) {
    if (node.tagName.split(':').pop() == 'Binary') {
      const text = (shaka.util.TXml.getTextContents(node) || '')
          .replace(/\s/g, '');
      if (!text || text.length % 4 || !/^[A-Za-z0-9+/]+={0,2}$/.test(text)) {
        return null;
      }
      return shaka.util.Uint8ArrayUtils.fromBase64(text);
    }
    for (const child of node.children) {
      if (typeof child != 'string') {
        const data = shaka.util.Scte35.binaryPayload_(child);
        if (data) {
          return data;
        }
      }
    }
    return null;
  }

  /**
   * @param {?string} text
   * @return {?Uint8Array}
   */
  static fromHex(text) {
    if (!text || !/^0[xX](?:[0-9a-fA-F]{2})+$/.test(text)) {
      return null;
    }
    return shaka.util.Uint8ArrayUtils.fromHex(text.substring(2));
  }
};

/** @private @const {!Array<string>} */
shaka.util.Scte35.SCHEMES_ = [
  'urn:scte:scte35:2013:xml',
  'urn:scte:scte35:2014:xml+bin',
  // Compatibility alias used by some producers.
  'urn:scte:scte35:2013:xml+bin',
  'urn:scte:scte35:2013:bin',
];