Source: lib/util/webm_generator.js

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

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

goog.require('shaka.mkv.BlockFlag');
goog.require('shaka.mkv.ElementId');
goog.require('shaka.mkv.TrackType');
goog.require('shaka.util.BufferUtils');
goog.require('shaka.util.SegmentGenerator');
goog.require('shaka.util.Uint8ArrayUtils');


/**
 * Writes the initialization and media segments that MSE plays for WebM: a
 * header with one track, and Clusters of SimpleBlocks.  It is what Matroska
 * needs to become WebM (VP8, Vorbis): WebM is a subset of it, and the frames
 * themselves are the same.  Blocks that lace several frames are the exception,
 * as MSE does not take lacing: each frame gets a block of its own.
 *
 * @implements {shaka.util.SegmentGenerator}
 */
shaka.util.WebmGenerator = class {
  /**
   * @param {shaka.util.WebmGenerator.StreamInfo} streamInfo
   */
  constructor(streamInfo) {
    /** @private {shaka.util.WebmGenerator.StreamInfo} */
    this.streamInfo_ = streamInfo;
  }

  /**
   * Generates an initialization segment.
   *
   * @override
   * @return {!Uint8Array}
   */
  initSegment() {
    const WebmGenerator = shaka.util.WebmGenerator;
    const ElementId = shaka.mkv.ElementId;

    const header = WebmGenerator.element_(ElementId.EBML,
        WebmGenerator.uint_(ElementId.EBML_VERSION, 1),
        WebmGenerator.uint_(ElementId.EBML_READ_VERSION, 1),
        WebmGenerator.uint_(ElementId.EBML_MAX_ID_LENGTH, 4),
        WebmGenerator.uint_(ElementId.EBML_MAX_SIZE_LENGTH, 8),
        WebmGenerator.string_(ElementId.DOC_TYPE, 'webm'),
        WebmGenerator.uint_(ElementId.DOC_TYPE_VERSION, 4),
        WebmGenerator.uint_(ElementId.DOC_TYPE_READ_VERSION, 2));
    const info = WebmGenerator.element_(ElementId.INFO,
        WebmGenerator.uint_(
            ElementId.TIMECODE_SCALE, this.streamInfo_.timecodeScale),
        WebmGenerator.string_(ElementId.MUXING_APP, 'Shaka Player'),
        WebmGenerator.string_(ElementId.WRITING_APP, 'Shaka Player'));
    const tracks = WebmGenerator.element_(
        ElementId.TRACKS, this.trackEntry_());
    // The Segment has no size, as it goes on for as long as media is appended.
    return shaka.util.Uint8ArrayUtils.concat(header,
        WebmGenerator.idBytes_(ElementId.SEGMENT),
        WebmGenerator.UNKNOWN_SIZE_, info, tracks);
  }

  /**
   * Generates a media segment: one or more Clusters.
   *
   * @override
   * @return {!Uint8Array}
   */
  segmentData() {
    const WebmGenerator = shaka.util.WebmGenerator;
    const ElementId = shaka.mkv.ElementId;
    const frames = this.streamInfo_.frames;

    const clusters = [];
    let start = 0;
    while (start < frames.length) {
      // A block writes its time as a signed 16-bit number of ticks after the
      // time of its Cluster, so a Cluster can only hold so much time.
      const clusterTimecode = frames[start].timecode;
      let end = start;
      const blocks = [];
      while (end < frames.length && frames[end].timecode - clusterTimecode <=
          WebmGenerator.MAX_CLUSTER_TICKS_) {
        blocks.push(this.simpleBlock_(frames[end], clusterTimecode));
        end++;
      }
      clusters.push(WebmGenerator.element_(ElementId.CLUSTER,
          WebmGenerator.uint_(ElementId.CLUSTER_TIMECODE, clusterTimecode),
          ...blocks));
      start = end;
    }
    return shaka.util.Uint8ArrayUtils.concat(...clusters);
  }

  /**
   * @return {!Uint8Array}
   * @private
   */
  trackEntry_() {
    const WebmGenerator = shaka.util.WebmGenerator;
    const ElementId = shaka.mkv.ElementId;
    const info = this.streamInfo_;
    const isVideo = info.type == shaka.mkv.TrackType.VIDEO;

    const fields = [
      WebmGenerator.uint_(ElementId.TRACK_NUMBER, info.trackNumber),
      // The identifier of the track; the number is as unique as anything.
      WebmGenerator.uint_(ElementId.TRACK_UID, info.trackNumber),
      WebmGenerator.uint_(ElementId.TRACK_TYPE, info.type),
      WebmGenerator.string_(ElementId.CODEC_ID, info.codecId),
    ];
    if (info.codecPrivate) {
      fields.push(WebmGenerator.element_(
          ElementId.CODEC_PRIVATE, info.codecPrivate));
    }
    if (isVideo) {
      fields.push(WebmGenerator.element_(ElementId.VIDEO,
          WebmGenerator.uint_(ElementId.PIXEL_WIDTH, info.width),
          WebmGenerator.uint_(ElementId.PIXEL_HEIGHT, info.height)));
    } else {
      fields.push(WebmGenerator.element_(ElementId.AUDIO,
          WebmGenerator.float_(ElementId.SAMPLING_FREQUENCY, info.sampleRate),
          WebmGenerator.uint_(ElementId.CHANNELS, info.channelCount)));
    }
    return WebmGenerator.element_(ElementId.TRACK_ENTRY, ...fields);
  }

  /**
   * @param {shaka.util.WebmGenerator.Frame} frame
   * @param {number} clusterTimecode
   * @return {!Uint8Array}
   * @private
   */
  simpleBlock_(frame, clusterTimecode) {
    const WebmGenerator = shaka.util.WebmGenerator;
    const relative = frame.timecode - clusterTimecode;
    // The track number is a variable-length integer, whose marker is part of
    // its value; a size is the same thing.
    const trackNumber = WebmGenerator.sizeVint_(this.streamInfo_.trackNumber);
    // The keyframe flag is set on every block of a track that has no other
    // kind (audio).
    const isKeyframe = frame.keyframe ||
        this.streamInfo_.type != shaka.mkv.TrackType.VIDEO;
    const flags = isKeyframe ? shaka.mkv.BlockFlag.KEYFRAME : 0;
    return WebmGenerator.element_(shaka.mkv.ElementId.SIMPLE_BLOCK,
        trackNumber,
        new Uint8Array([(relative >> 8) & 0xff, relative & 0xff, flags]),
        frame.data);
  }

  /**
   * @param {number} id
   * @param {...!Uint8Array} payload
   * @return {!Uint8Array}
   * @private
   */
  static element_(id, ...payload) {
    const WebmGenerator = shaka.util.WebmGenerator;
    const body = shaka.util.Uint8ArrayUtils.concat(...payload);
    return shaka.util.Uint8ArrayUtils.concat(
        WebmGenerator.idBytes_(id), WebmGenerator.sizeVint_(body.length), body);
  }

  /**
   * @param {number} id
   * @param {number} value
   * @return {!Uint8Array}
   * @private
   */
  static uint_(id, value) {
    const bytes = [];
    let rest = value;
    do {
      bytes.unshift(rest % 256);
      rest = Math.floor(rest / 256);
    } while (rest > 0);
    return shaka.util.WebmGenerator.element_(id, new Uint8Array(bytes));
  }

  /**
   * @param {number} id
   * @param {number} value
   * @return {!Uint8Array}
   * @private
   */
  static float_(id, value) {
    const bytes = new Uint8Array(8);
    shaka.util.BufferUtils.toDataView(bytes).setFloat64(0, value);
    return shaka.util.WebmGenerator.element_(id, bytes);
  }

  /**
   * @param {number} id
   * @param {string} value
   * @return {!Uint8Array}
   * @private
   */
  static string_(id, value) {
    return shaka.util.WebmGenerator.element_(id, new Uint8Array(
        Array.from(value).map((char) => char.charCodeAt(0))));
  }

  /**
   * @param {number} id An element ID, with its length marker.
   * @return {!Uint8Array}
   * @private
   */
  static idBytes_(id) {
    const bytes = [];
    for (let rest = id; rest > 0; rest = Math.floor(rest / 256)) {
      bytes.unshift(rest % 256);
    }
    return new Uint8Array(bytes);
  }

  /**
   * Writes a variable-length integer with as few bytes as it can: the number
   * of leading zeros of the first byte is the number of bytes that follow it.
   *
   * @param {number} value
   * @return {!Uint8Array}
   * @private
   */
  static sizeVint_(value) {
    for (let length = 1; length <= 8; length++) {
      // All the bits set is the reserved "unknown" value.
      if (value < Math.pow(2, 7 * length) - 1) {
        const bytes = new Uint8Array(length);
        let rest = value;
        for (let i = length - 1; i >= 0; i--) {
          bytes[i] = rest % 256;
          rest = Math.floor(rest / 256);
        }
        bytes[0] |= 0x80 >> (length - 1);
        return bytes;
      }
    }
    throw new RangeError('The value does not fit an EBML integer');
  }
};


/**
 * @typedef {{
 *   timecode: number,
 *   data: !Uint8Array,
 *   keyframe: boolean,
 * }}
 *
 * @property {number} timecode
 *   The time, in ticks of the timecode scale of the stream.
 * @property {!Uint8Array} data
 * @property {boolean} keyframe
 */
shaka.util.WebmGenerator.Frame;


/**
 * @typedef {{
 *   trackNumber: number,
 *   type: number,
 *   codecId: string,
 *   codecPrivate: ?Uint8Array,
 *   timecodeScale: number,
 *   width: number,
 *   height: number,
 *   channelCount: number,
 *   sampleRate: number,
 *   frames: !Array<shaka.util.WebmGenerator.Frame>,
 * }}
 *
 * @property {number} trackNumber
 * @property {number} type
 *   A shaka.mkv.TrackType.
 * @property {string} codecId
 *   The Matroska CodecID.
 * @property {?Uint8Array} codecPrivate
 * @property {number} timecodeScale
 *   Nanoseconds per tick.
 * @property {number} width
 *   Of a video track.
 * @property {number} height
 *   Of a video track.
 * @property {number} channelCount
 *   Of an audio track.
 * @property {number} sampleRate
 *   Of an audio track.
 * @property {!Array<shaka.util.WebmGenerator.Frame>} frames
 *   The frames of the media segment.
 */
shaka.util.WebmGenerator.StreamInfo;


/**
 * The size that EBML writes to say that an element goes on to the end: 8 bytes,
 * all the bits of the value set.
 *
 * @private @const {!Uint8Array}
 */
shaka.util.WebmGenerator.UNKNOWN_SIZE_ =
    new Uint8Array([0x01, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]);


/**
 * How far apart in time the first and the last block of a Cluster may be.  The
 * time of a block is a signed 16-bit number of ticks after that of its Cluster,
 * so this leaves room for the difference to be rounded.
 *
 * @private @const {number}
 */
shaka.util.WebmGenerator.MAX_CLUSTER_TICKS_ = 30000;