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.StringUtils');
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.Scte35.parseXml_(
              shaka.util.StringUtils.fromBytesAutoDetect(emsg.messageData));
        } catch (error) {}
        if (node) {
          event.data = shaka.util.Scte35.binaryPayload_(node);
          if (!event.data) {
            event.node = node;
          }
        }
      }
    }
    return event;
  }

  /**
   * Reads one record of an MSF event timeline track carrying SCTE-35
   * (draft-wilaw-moq-scte35-event-timeline), once the caller has placed it on
   * the presentation timeline.
   *
   * @param {string} eventType The track's catalog `eventType`.
   * @param {string} payload The record's `scte35_payload`: base64 for binary
   *   carriage, the XML itself for XML carriage.
   * @param {number} startTime
   * @param {string} id
   * @return {?shaka.extern.Scte35Event} Null when the event type is not an
   *   SCTE-35 one, or the payload cannot be decoded.
   */
  static fromMsf(eventType, payload, startTime, id) {
    const format = shaka.util.Scte35.MSF_EVENT_TYPES_.get(eventType.trim());
    if (!format) {
      return null;
    }
    const event = {
      schemeIdUri: eventType,
      startTime,
      // The envelope carries no duration; it is only inside the payload.
      endTime: startTime,
      id,
      source: 'msf',
      kind: '',
      data: null,
      node: null,
    };
    if (format == 'bin') {
      event.data = shaka.util.Scte35.fromBase64_(payload);
      return event.data ? event : null;
    }
    const node = shaka.util.Scte35.parseXml_(payload);
    if (!node) {
      return null;
    }
    event.data = shaka.util.Scte35.binaryPayload_(node);
    if (!event.data) {
      event.node = node;
    }
    return event;
  }

  /**
   * Whether an MSF event timeline `eventType` is one fromMsf() reads.
   *
   * @param {string} eventType
   * @return {boolean}
   */
  static isMsfEventType(eventType) {
    return shaka.util.Scte35.MSF_EVENT_TYPES_.has(eventType.trim());
  }

  /**
   * The first element of an XML message, or null when there is none.
   *
   * TXml.parseXmlString() answers the first node it finds, which for text
   * that is not XML at all is the text itself rather than an element, and for
   * a document with an XML declaration is the declaration.
   *
   * @param {string} text
   * @return {?shaka.extern.xml.Node}
   * @private
   */
  static parseXml_(text) {
    // A message we cannot even parse must not fail playback.
    try {
      for (const node of shaka.util.TXml.parse(text, false) || []) {
        if (typeof node != 'string' && !node.tagName.startsWith('?')) {
          return node;
        }
      }
    } catch (error) {}
    return null;
  }

  /**
   * 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') {
      return shaka.util.Scte35.fromBase64_(
          shaka.util.TXml.getTextContents(node));
    }
    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}
   * @private
   */
  static fromBase64_(text) {
    text = (text || '').replace(/\s/g, '');
    if (!text || text.length % 4 || !/^[A-Za-z0-9+/]+={0,2}$/.test(text)) {
      return null;
    }
    return shaka.util.Uint8ArrayUtils.fromBase64(text);
  }

  /**
   * @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',
];

/**
 * The MSF event timeline types carrying SCTE-35, and whether their payload is
 * base64 binary or XML.  draft-wilaw-moq-scte35-event-timeline defines the
 * 2022 names; the MSF draft's own catalog example uses the 2013 one.  These
 * are kept apart from SCHEMES_ so that recognizing them in MSF does not change
 * what DASH and emsg recognize.
 *
 * @private @const {!Map<string, string>}
 */
shaka.util.Scte35.MSF_EVENT_TYPES_ = new Map([
  ['urn:scte:scte35:2022:bin', 'bin'],
  ['urn:scte:scte35:2022:xml', 'xml'],
  ['urn:scte:scte35:2013:bin', 'bin'],
  ['urn:scte:scte35:2013:xml', 'xml'],
  ['urn:scte:scte35:2014:xml+bin', 'xml'],
  ['urn:scte:scte35:2013:xml+bin', 'xml'],
]);