Source: lib/msf/drafts/draft18/messages.js

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

goog.provide('shaka.msf.draft18.MessageType');
goog.provide('shaka.msf.draft18.MessageTypeId');
goog.provide('shaka.msf.draft18.MessageWriter');

goog.require('shaka.msf.Utils');
goog.require('shaka.util.BufferUtils');
goog.require('shaka.util.DataViewWriter');
goog.require('shaka.util.StringUtils');


/**
 * Message type IDs for draft-18.
 *
 * Deliberately owned by this draft family rather than shared. The drafts
 * reassign IDs and, more dangerously, reuse them with different meanings: 0x7
 * was PUBLISH_NAMESPACE_OK and is now the generic REQUEST_OK, 0x8 was
 * PUBLISH_NAMESPACE_ERROR and is now NAMESPACE, 0xE was TRACK_STATUS_OK and is
 * now NAMESPACE_DONE, 0xF was TRACK_STATUS_ERROR and is now PUBLISH_BLOCKED,
 * and SUBSCRIBE_NAMESPACE moved from 0x11 to 0x50. A shared enum would
 * mis-parse silently instead of failing.
 *
 * Note the values are var ints, not single bytes: SETUP is 0x2F00.
 *
 * @enum {number}
 */
shaka.msf.draft18.MessageTypeId = {
  SETUP: 0x2f00,
  GOAWAY: 0x10,
  SUBSCRIBE: 0x3,
  SUBSCRIBE_OK: 0x4,
  PUBLISH: 0x1d,
  PUBLISH_OK: 0x1e,
  PUBLISH_DONE: 0xb,
  FETCH: 0x16,
  FETCH_OK: 0x18,
  TRACK_STATUS: 0xd,
  PUBLISH_NAMESPACE: 0x6,
  SUBSCRIBE_NAMESPACE: 0x50,
  SUBSCRIBE_TRACKS: 0x51,
  NAMESPACE: 0x8,
  NAMESPACE_DONE: 0xe,
  PUBLISH_BLOCKED: 0xf,
  REQUEST_UPDATE: 0x2,
  REQUEST_OK: 0x7,
  REQUEST_ERROR: 0x5,
};


/**
 * Internal discriminators for decoded draft-18 messages.
 *
 * @enum {string}
 */
shaka.msf.draft18.MessageType = {
  SETUP: 'setup',
  GOAWAY: 'goaway',
  SUBSCRIBE: 'subscribe',
  SUBSCRIBE_OK: 'subscribe_ok',
  PUBLISH: 'publish',
  PUBLISH_OK: 'publish_ok',
  PUBLISH_DONE: 'publish_done',
  FETCH: 'fetch',
  FETCH_OK: 'fetch_ok',
  TRACK_STATUS: 'track_status',
  PUBLISH_NAMESPACE: 'publish_namespace',
  SUBSCRIBE_NAMESPACE: 'subscribe_namespace',
  SUBSCRIBE_TRACKS: 'subscribe_tracks',
  NAMESPACE: 'namespace',
  NAMESPACE_DONE: 'namespace_done',
  PUBLISH_BLOCKED: 'publish_blocked',
  REQUEST_UPDATE: 'request_update',
  REQUEST_OK: 'request_ok',
  REQUEST_ERROR: 'request_error',
};


/**
 * Fetch Type values, from draft-18 section 10.12.
 *
 * @enum {number}
 */
shaka.msf.draft18.FetchType = {
  STANDALONE: 0x1,
  RELATIVE_JOINING: 0x2,
  ABSOLUTE_JOINING: 0x3,
};


/**
 * Serializes draft-18 control messages.
 *
 * Every message is a var int type, a 16-bit length, then the payload. Two
 * things differ structurally from draft-16 beyond the field layouts:
 *
 *  - The type is a var int rather than a single byte, because SETUP is 0x2F00.
 *  - Responses carry no Request ID. They arrive on the bidirectional stream
 *    that carried the request, and that stream is the correlation, so there is
 *    nothing to write.
 *
 * Draft-20 changed one message body and nothing else, so the field writers and
 * the type/length framing are protected rather than private: draft-20 extends
 * this class and overrides only marshalFetch().
 */
shaka.msf.draft18.MessageWriter = class {
  /**
   * @param {!shaka.extern.MsfCodec} codec
   * @param {number=} initialSize
   */
  constructor(codec, initialSize = 1024) {
    /** @private {!shaka.extern.MsfCodec} */
    this.codec_ = codec;

    /** @private {!shaka.util.DataViewWriter} */
    this.writer_ = new shaka.util.DataViewWriter(
        initialSize, shaka.util.DataViewWriter.Endianness.BIG_ENDIAN);
  }

  /**
   * @return {!Uint8Array}
   */
  getBytes() {
    return this.writer_.getBytes();
  }

  /**
   * @return {!shaka.extern.MsfCodec}
   * @protected
   */
  getCodec() {
    return this.codec_;
  }

  /** Resets the buffer to start writing from the beginning. */
  reset() {
    this.writer_.reset();
  }

  /**
   * @param {bigint|number} value
   * @protected
   */
  writeVarInt(value) {
    this.codec_.encodeVarInt(
        this.writer_, typeof value == 'bigint' ? value : BigInt(value));
  }

  /**
   * @param {string} str
   * @protected
   */
  writeString(str) {
    const bytes = shaka.util.BufferUtils.toUint8(
        shaka.util.StringUtils.toUTF8(str));
    this.writeVarInt(bytes.length);
    this.writer_.writeBytes(bytes);
  }

  /**
   * A Track Namespace is a count of fields followed by each length-prefixed
   * field.
   *
   * @param {Array<string>} namespace
   * @protected
   */
  writeNamespace(namespace) {
    const fields = namespace || [];
    this.writeVarInt(fields.length);
    for (const field of fields) {
      this.writeString(field);
    }
  }

  /**
   * @param {shaka.msf.Utils.Location} location
   * @protected
   */
  writeLocation(location) {
    this.writeVarInt(location.group);
    this.writeVarInt(location.object);
  }

  /**
   * Writes delta-encoded Key-Value-Pairs without a count prefix, which is how
   * Setup Options are framed: they span the whole message payload and are
   * bounded by the message length.
   *
   * Draft-22 gave LOCATION_FILTER a value that leads with its own Location
   * Filter Type rather than a length, so a parameter list asks
   * getParameterEncoding() whether an odd type's value carries a length.
   * Setup Options always do.
   *
   * @param {!Array<shaka.msf.Utils.KeyValuePair>} pairs
   * @param {boolean=} isParameterList
   * @protected
   */
  writeKeyValuePairs(pairs, isParameterList = false) {
    const Encoding = shaka.msf.draft18.MessageWriter.ParameterEncoding;

    // Delta encoding requires ascending type order.
    /** @type {!Array<shaka.msf.Utils.KeyValuePair>} */
    const sorted = [...pairs].sort(
        (a, b) => (a.type < b.type ? -1 : (a.type > b.type ? 1 : 0)));

    let prevType = BigInt(0);
    for (const pair of sorted) {
      this.writeVarInt(pair.type - prevType);
      prevType = pair.type;

      if (pair.type % BigInt(2) === BigInt(0)) {
        if (typeof pair.value !== 'bigint') {
          throw new Error(
              `Invalid value for even key ${pair.type}: expected bigint`);
        }
        this.writeVarInt(pair.value);
      } else {
        if (!ArrayBuffer.isView(pair.value)) {
          throw new Error(
              `Invalid value for odd key ${pair.type}: expected Uint8Array`);
        }
        const bytes = /** @type {!Uint8Array} */ (pair.value);
        const selfDelimited = isParameterList &&
            this.getParameterEncoding(Number(pair.type)) ==
                Encoding.LOCATION_FILTER;
        if (!selfDelimited) {
          this.writeVarInt(bytes.byteLength);
        }
        this.writer_.writeBytes(bytes);
      }
    }
  }

  /**
   * Writes a count-prefixed parameter list, which is how every message other
   * than SETUP frames its parameters.
   *
   * @param {(Array<shaka.msf.Utils.KeyValuePair>|undefined)} pairs
   * @protected
   */
  writeParameters(pairs) {
    const params = pairs || [];
    this.writeVarInt(params.length);
    this.writeKeyValuePairs(params, /* isParameterList= */ true);
  }

  /**
   * How this draft encodes the value of a Message Parameter type, or null for
   * a type it does not define. Since draft-18 that is set per type rather
   * than by the parity rule Properties use, so a type nobody told us about
   * cannot be stepped over when reading a response.
   *
   * @param {number} type
   * @return {?shaka.msf.draft18.MessageWriter.ParameterEncoding}
   */
  getParameterEncoding(type) {
    return shaka.msf.draft18.MessageWriter.PARAMETER_ENCODINGS_.get(type) ??
        null;
  }

  /**
   * Writes the var int type and 16-bit length around a payload.
   *
   * @param {shaka.msf.draft18.MessageTypeId} type
   * @param {function()} writeContent
   * @return {!shaka.msf.draft18.MessageWriter}
   * @protected
   */
  marshal(type, writeContent) {
    this.writeVarInt(type);

    const lengthPosition = this.writer_.getPosition();
    this.writer_.writeUint16(0); // Placeholder

    const contentStart = this.writer_.getPosition();
    writeContent();
    this.writer_.patchUint16(
        lengthPosition, this.writer_.getPosition() - contentStart);
    return this;
  }

  /**
   * Draft-17 collapsed CLIENT_SETUP and SERVER_SETUP into one message whose
   * payload is nothing but Setup Options, uncounted and bounded by the
   * message length.
   *
   * @param {!Array<shaka.msf.Utils.KeyValuePair>} options
   * @return {!shaka.msf.draft18.MessageWriter}
   */
  marshalSetup(options) {
    return this.marshal(shaka.msf.draft18.MessageTypeId.SETUP, () => {
      this.writeKeyValuePairs(options);
    });
  }

  /**
   * @param {{
   *   requestId: bigint,
   *   namespace: Array<string>,
   *   trackName: string,
   *   params: (Array<shaka.msf.Utils.KeyValuePair>|undefined),
   * }} msg
   * @return {!shaka.msf.draft18.MessageWriter}
   */
  marshalSubscribe(msg) {
    return this.marshal(shaka.msf.draft18.MessageTypeId.SUBSCRIBE, () => {
      this.writeVarInt(msg.requestId);
      this.writeNamespace(msg.namespace);
      this.writeString(msg.trackName);
      this.writeParameters(msg.params);
    });
  }

  /**
   * A standalone FETCH. Start and End are Location structures rather than the
   * four loose var ints draft-16 used, and the range is a field of the message
   * rather than a parameter, which is what draft-20 changed.
   *
   * The range is an inclusive pair of absolute Locations, so the zeroed pair
   * the caller may leave out asks for exactly the Object at {0, 0}, not for
   * the whole track. Draft-20 says the same thing with a LOCATION_FILTER.
   *
   * @param {{
   *   requestId: bigint,
   *   namespace: Array<string>,
   *   trackName: string,
   *   startLocation: (shaka.msf.Utils.Location|undefined),
   *   endLocation: (shaka.msf.Utils.Location|undefined),
   *   params: (Array<shaka.msf.Utils.KeyValuePair>|undefined),
   * }} msg
   * @return {!shaka.msf.draft18.MessageWriter}
   */
  marshalFetch(msg) {
    const range = shaka.msf.draft18.MessageWriter.fetchRange(msg);
    return this.marshal(shaka.msf.draft18.MessageTypeId.FETCH, () => {
      this.writeVarInt(msg.requestId);
      this.writeVarInt(shaka.msf.draft18.FetchType.STANDALONE);
      this.writeNamespace(msg.namespace);
      this.writeString(msg.trackName);
      this.writeLocation(range.start);
      this.writeLocation(range.end);
      this.writeParameters(msg.params);
    });
  }

  /**
   * A Relative Joining FETCH: the Groups leading up to a subscription's
   * Joining Location, contiguous with what the subscription delivers after it
   * (draft-18 section 10.12.2). Joining Start 0 asks for the current Group
   * from its first Object.
   *
   * @param {{
   *   requestId: bigint,
   *   joiningRequestId: bigint,
   *   joiningStart: bigint,
   *   params: (Array<shaka.msf.Utils.KeyValuePair>|undefined),
   * }} msg
   * @return {!shaka.msf.draft18.MessageWriter}
   */
  marshalJoiningFetch(msg) {
    return this.marshal(shaka.msf.draft18.MessageTypeId.FETCH, () => {
      this.writeVarInt(msg.requestId);
      this.writeVarInt(shaka.msf.draft18.FetchType.RELATIVE_JOINING);
      this.writeVarInt(msg.joiningRequestId);
      this.writeVarInt(msg.joiningStart);
      this.writeParameters(msg.params);
    });
  }

  /**
   * The SUBSCRIBE parameter that also delivers the current Group from its
   * first Object, or null when this draft has none and a Joining FETCH has to
   * be sent instead, as it does in draft-18.
   *
   * @return {?shaka.msf.Utils.KeyValuePair}
   */
  fillCurrentGroupParam() {
    return null;
  }

  /**
   * The parameter that makes a SUBSCRIBE start at a given Location rather than
   * at the live edge, which is what seeking into already-published content
   * needs.
   *
   * Draft-18 spells it SUBSCRIPTION_FILTER (0x21): a Filter Type followed by
   * the fields that type takes. AbsoluteStart takes a Start Location and no
   * end, so the subscription runs from there to the live edge and onwards, and
   * the draft says outright that the Start Location MAY be smaller than the
   * publisher's Largest Object (section 5.1.2). Draft-20 keeps the parameter
   * type and changes its contents, so the draft-20 writer overrides this.
   *
   * @param {shaka.msf.Utils.Location} startLocation
   * @return {shaka.msf.Utils.KeyValuePair}
   */
  locationFilterParam(startLocation) {
    return {
      type: BigInt(shaka.msf.draft18.MessageWriter.Parameter.LOCATION_FILTER),
      value: this.encodeVarIntField([
        BigInt(shaka.msf.draft18.MessageWriter.FilterType.ABSOLUTE_START),
        startLocation.group,
        startLocation.object,
      ]),
    };
  }

  /**
   * The Location range a FETCH covers, filling in the default the callers that
   * do not care rely on. Shared with draft-20, which carries the same range in
   * a parameter instead of in the message.
   *
   * @param {{
   *   startLocation: (shaka.msf.Utils.Location|undefined),
   *   endLocation: (shaka.msf.Utils.Location|undefined),
   * }} msg
   * @return {{
   *   start: shaka.msf.Utils.Location,
   *   end: shaka.msf.Utils.Location,
   * }}
   * @protected
   */
  static fetchRange(msg) {
    const origin = {group: BigInt(0), object: BigInt(0)};
    return {
      start: msg.startLocation || origin,
      end: msg.endLocation || origin,
    };
  }

  /**
   * Encodes a sequence of var ints into a standalone byte field, for the
   * parameters whose value is itself a var int structure rather than a single
   * integer.
   *
   * @param {!Array<bigint>} values
   * @return {!Uint8Array}
   * @protected
   */
  encodeVarIntField(values) {
    // Nine bytes is the widest a var int gets in this draft family.
    const writer = new shaka.util.DataViewWriter(
        9 * values.length, shaka.util.DataViewWriter.Endianness.BIG_ENDIAN);
    for (const value of values) {
      this.codec_.encodeVarInt(writer, value);
    }
    return writer.getBytes();
  }

  /**
   * @param {{
   *   requestId: bigint,
   *   namespace: Array<string>,
   *   params: (Array<shaka.msf.Utils.KeyValuePair>|undefined),
   * }} msg
   * @return {!shaka.msf.draft18.MessageWriter}
   */
  marshalSubscribeNamespace(msg) {
    return this.marshal(
        shaka.msf.draft18.MessageTypeId.SUBSCRIBE_NAMESPACE, () => {
          this.writeVarInt(msg.requestId);
          this.writeNamespace(msg.namespace);
          this.writeParameters(msg.params);
        });
  }

  /**
   * REQUEST_OK answers PUBLISH_NAMESPACE, SUBSCRIBE_NAMESPACE, TRACK_STATUS
   * and the rest. It carries no Request ID: it goes back on the request's own
   * bidirectional stream.
   *
   * @param {(Array<shaka.msf.Utils.KeyValuePair>|undefined)=} params
   * @return {!shaka.msf.draft18.MessageWriter}
   */
  marshalRequestOk(params) {
    return this.marshal(shaka.msf.draft18.MessageTypeId.REQUEST_OK, () => {
      this.writeParameters(params);
      // Track Properties are empty for everything but TRACK_STATUS_OK.
    });
  }
};


/**
 * The parameter types this player sends on a SUBSCRIBE or a FETCH.
 *
 * Draft-20 renamed 0x21 from SUBSCRIPTION_FILTER to LOCATION_FILTER and
 * changed what its value holds, but the code point is the same in both, so
 * there is one constant rather than one per draft.
 *
 * @enum {number}
 */
shaka.msf.draft18.MessageWriter.Parameter = {
  FORWARD: 0x10,
  SUBSCRIBER_PRIORITY: 0x20,
  LOCATION_FILTER: 0x21,
  FILL_PARAMETERS: 0x23,
};


/**
 * The SUBSCRIPTION_FILTER Filter Types this player sends (draft-18 section
 * 5.1.2). Draft-20 and draft-21 have no Filter Type, and draft-22 numbers its
 * own differently.
 *
 * @enum {number}
 */
shaka.msf.draft18.MessageWriter.FilterType = {
  ABSOLUTE_START: 0x3,
};


/**
 * How a Message Parameter's value is encoded (draft-18 section 10.2).
 *
 * @enum {number}
 */
shaka.msf.draft18.MessageWriter.ParameterEncoding = {
  UINT8: 0,
  VARINT: 1,
  LOCATION: 2,
  LENGTH_PREFIXED: 3,
  // A Location Filter Type followed by the var ints that type selects, with
  // no length (draft-22 section 9.20.9). Only draft-22 uses it.
  LOCATION_FILTER: 4,
};


/**
 * The encoding of the Message Parameter types defined across draft-18,
 * draft-20 and draft-21. The types are numbers rather than bigint so that
 * building this map needs no BigInt at load time.
 *
 * @private @const {!Map<number,
 *     shaka.msf.draft18.MessageWriter.ParameterEncoding>}
 */
shaka.msf.draft18.MessageWriter.PARAMETER_ENCODINGS_ = new Map([
  [0x02, shaka.msf.draft18.MessageWriter.ParameterEncoding.VARINT],
  [0x03, shaka.msf.draft18.MessageWriter.ParameterEncoding.LENGTH_PREFIXED],
  [0x06, shaka.msf.draft18.MessageWriter.ParameterEncoding.VARINT],
  [0x08, shaka.msf.draft18.MessageWriter.ParameterEncoding.VARINT],
  [0x09, shaka.msf.draft18.MessageWriter.ParameterEncoding.LOCATION],
  [0x10, shaka.msf.draft18.MessageWriter.ParameterEncoding.UINT8],
  [0x20, shaka.msf.draft18.MessageWriter.ParameterEncoding.UINT8],
  [0x21, shaka.msf.draft18.MessageWriter.ParameterEncoding.LENGTH_PREFIXED],
  [0x22, shaka.msf.draft18.MessageWriter.ParameterEncoding.UINT8],
  [0x23, shaka.msf.draft18.MessageWriter.ParameterEncoding.LENGTH_PREFIXED],
  [0x32, shaka.msf.draft18.MessageWriter.ParameterEncoding.VARINT],
  [0x35, shaka.msf.draft18.MessageWriter.ParameterEncoding.UINT8],
]);