Source: lib/msf/msf_receiver.js

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

goog.provide('shaka.msf.Receiver');

goog.require('shaka.log');
goog.require('shaka.msf.Utils');

goog.requireType('shaka.msf.Reader');


shaka.msf.Receiver = class {
  /**
   * @param {!shaka.msf.Reader} reader
   */
  constructor(reader) {
    /** @private {!shaka.msf.Reader} */
    this.reader_ = reader;
  }

  /**
   * @return {!Promise<shaka.msf.Utils.ServerSetup>}
   */
  async server() {
    const SetupType = shaka.msf.Utils.SetupType;
    shaka.log.debug('Decoding server setup message...');

    const type = await this.reader_.u53();
    shaka.log.debug(`Setup message type: 0x${type.toString(16)}
        (expected 0x${SetupType.SERVER.toString(16)})`);

    if (type !== SetupType.SERVER) {
      const errorMsg =
          `Server SETUP type must be ${SetupType.SERVER}, got ${type}`;
      shaka.log.error(errorMsg);
      throw new Error(errorMsg);
    }

    // Read the 16-bit MSB length field
    const lengthBytes = await this.reader_.read(2);
    const messageLength = (lengthBytes[0] << 8) | lengthBytes[1]; // MSB format
    shaka.log.debug(`Message length (16-bit MSB): ${messageLength} bytes`);

    // Store the current position to validate length later
    const startPosition = this.reader_.getByteLength();

    const version = await this.reader_.u53();
    shaka.log.debug(`Server selected version: 0x${version.toString(16)}`);

    const params = await this.parameters();
    shaka.log.v1(
        `Parameters: ${params ? `${params.length} parameters` : 'none'}`);

    // Log each parameter in detail
    if (params && params.length > 0) {
      for (const param of params) {
        if (typeof param.value === 'number') {
          shaka.log.v1(
              `Parameter ID: ${param.type}, value: ${param.value} (number)`);
        } else {
          shaka.log.v1(`Parameter ID: ${param.type}, length:
              ${param.value.byteLength} bytes, value:
              ${this.formatBytes_(param.value)}`);
        }
      }
    }

    // Validate that we read the expected number of bytes
    const endPosition = this.reader_.getByteLength();
    const bytesRead = startPosition - endPosition;

    if (bytesRead !== messageLength) {
      const warningMsg = `Message length mismatch: expected ${messageLength}
          bytes, read ${bytesRead} bytes`;
      shaka.log.warning(warningMsg);
      // Not throwing an error here as we've already read the data
    }

    const result = {
      version,
      params,
    };

    shaka.log.debug('Server setup message decoded:', result);
    return result;
  }

  /**
   * @param {!Uint8Array} bytes
   * @return {string}
   * @private
   */
  formatBytes_(bytes) {
    if (bytes.length <= 16) {
      return Array.from(bytes)
          .map((b) => b.toString(16).padStart(2, '0'))
          .join(' ');
    } else {
      const start = Array.from(bytes.slice(0, 8))
          .map((b) => b.toString(16).padStart(2, '0'))
          .join(' ');
      const end = Array.from(bytes.slice(-8))
          .map((b) => b.toString(16).padStart(2, '0'))
          .join(' ');
      return `${start} ... ${end} (${bytes.length} bytes total)`;
    }
  }

  /**
   * @return {!Promise<(Array<shaka.msf.Utils.KeyValuePair> | undefined)>}
   */
  async parameters() {
    const countResult = await this.reader_.u53WithSize();
    const count = countResult.value;

    shaka.log.v1(`Parameter count: ${count}, count field:
        ${countResult.bytesRead} bytes`);

    if (count === 0) {
      return undefined;
    }

    const params = [];

    for (let i = 0; i < count; i++) {
      // Read parameter type (key)
      // eslint-disable-next-line no-await-in-loop
      const typeResult = await this.reader_.u62WithSize();
      const paramType = typeResult.value;

      // Check if the type is even or odd
      const isEven = paramType % 2 === 0;

      if (isEven) {
        // Even type: value is a single var int
        // eslint-disable-next-line no-await-in-loop
        const valueResult = await this.reader_.u62WithSize();
        const value = valueResult.value;
        shaka.log.v1(`Parameter ${i + 1}/${count}: Type ${paramType} (even),
            Value: ${value} (${valueResult.bytesRead} bytes)`);

        // Check for duplicates
        const existingIndex = params.findIndex((p) => p.type === paramType);
        if (existingIndex !== -1) {
          shaka.log.warning(`Duplicate parameter type: ${paramType},
              overwriting previous value`);
          params.splice(existingIndex, 1);
        }

        params.push({type: paramType, value});
      } else {
        // Odd type: value is a byte sequence with length
        // eslint-disable-next-line no-await-in-loop
        const lengthResult = await this.reader_.u53WithSize();
        const length = lengthResult.value;

        // Check maximum length (2^16-1)
        if (length > 65535) {
          const errorMsg =
              `Parameter value length exceeds maximum: ${length} > 65535`;
          shaka.log.error(errorMsg);
          throw new Error(errorMsg);
        }

        // Read the value bytes
        // eslint-disable-next-line no-await-in-loop
        const value = await this.reader_.read(length);
        // At this point, value should always be a Uint8Array
        shaka.log.v1(`Parameter ${i + 1}/${count}: Type ${paramType} (odd),
            Length: ${length}, Value: ${this.formatBytes_(value)}`);

        // Check for duplicates
        const existingIndex = params.findIndex((p) => p.type === paramType);
        if (existingIndex !== -1) {
          shaka.log.warning(`Duplicate parameter type: ${paramType},
              overwriting previous value`);
          params.splice(existingIndex, 1);
        }

        params.push({type: paramType, value});
      }
    }

    return params;
  }
};