Source: lib/msf/packaging/locmaf.js

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

goog.provide('shaka.msf.packaging.Locmaf');

goog.require('goog.asserts');
goog.require('shaka.log');
goog.require('shaka.media.InitSegmentReference');
goog.require('shaka.media.SegmentUtils');
goog.require('shaka.msf.LOCMAFParser');
goog.require('shaka.msf.PackagingRegistry');
goog.require('shaka.msf.QuicVarIntCodec');
goog.require('shaka.util.Mp4BoxParsers');
goog.require('shaka.util.Mp4Parser');


/**
 * Packaging for LOCMAF tracks, where every MoQT object is one CMAF chunk with
 * its `moof` reduced to a handful of tagged fields.
 *
 * The media is the same as a `cmaf` track's -- the same initialization
 * segment, the same sample data, the same DRM signaling -- so everything
 * about how the track presents itself is shared with that packaging. What
 * differs is that an object is not appendable as it stands: the `moof` has to
 * be rebuilt from the fields ahead of the sample data, and a chunk that
 * carries only what changed since the previous one cannot be read without it.
 * All of that lives in shaka.msf.LOCMAFParser; this class supplies it the
 * three things the initialization segment contributes and nothing else.
 *
 * A catalog may offer one rendition under `locmaf` and `cmaf` at the same
 * time, sharing a single initialization-data entry: they are alternative
 * encodings of one source rather than distinct media, and nothing in the
 * catalog marks either as the one to use. Registering this packaging
 * therefore makes both visible, doubling the variant list for such a catalog.
 * Which encoding to keep is the application's call, made by dropping the
 * others in `manifest.msf.catalogPreprocessor`; the demo's
 * `shakaAssets.preferLocmafTracks` is a worked example.
 *
 * @see https://datatracker.ietf.org/doc/draft-einarsson-moq-locmaf/
 *
 * @implements {shaka.extern.MsfPackaging}
 * @final
 */
shaka.msf.packaging.Locmaf = class {
  constructor() {
    /** @private {?shaka.msf.LOCMAFParser.TrackParams} */
    this.params_ = null;
  }

  /**
   * @override
   */
  describeTrack(track, initData) {
    // A version this parser does not know reinterprets wire syntax it would
    // otherwise decode into plausible nonsense, which the bytes themselves
    // give no way to detect. The catalog offers the same source under `cmaf`,
    // so skipping the track is the graceful outcome.
    if (track.locmafVersion !== shaka.msf.packaging.Locmaf.VERSION) {
      shaka.log.warning(`Skipping LOCMAF track "${track.name}" with ` +
          `unsupported locmafVersion "${track.locmafVersion}"; this build ` +
          `supports "${shaka.msf.packaging.Locmaf.VERSION}".`);
      return null;
    }

    if (!initData.byteLength) {
      shaka.log.warning(`Skipping LOCMAF track "${track.name}" with no ` +
          'initialization data; a LOCMAF chunk cannot be reconstructed ' +
          'without the trex defaults its fields fall back to.');
      return null;
    }

    const basicInfo = shaka.media.SegmentUtils.getBasicInfoFromMp4(
        initData, initData, /* disableText= */ false);
    if (!basicInfo) {
      return null;
    }

    const timescale = basicInfo.timescale || track.timescale;
    if (!timescale) {
      shaka.log.info(
          'Skipping incompatible track due missing timescale', track);
      return null;
    }

    const params =
        shaka.msf.packaging.Locmaf.readTrackParams_(initData, timescale);
    if (!params) {
      shaka.log.warning(`Skipping LOCMAF track "${track.name}" whose ` +
          'initialization segment carries no trex defaults.');
      return null;
    }
    this.params_ = params;

    return {
      basicInfo,
      initSegmentReference: new shaka.media.InitSegmentReference(
          () => [],
          /* startBytes= */ 0,
          /* endBytes= */ null,
          /* mediaQuality= */ null,
          timescale,
          initData),
    };
  }

  /**
   * @override
   */
  createSegmenter(codec) {
    goog.asserts.assert(this.params_, 'describeTrack() must run first');
    // A caller with no session -- a test, or a packaging driven directly --
    // gets the encoding every draft up to 16 used.
    return new shaka.msf.packaging.LocmafSegmenter(
        new shaka.msf.LOCMAFParser(
            codec || new shaka.msf.QuicVarIntCodec(),
            /** @type {!shaka.msf.LOCMAFParser.TrackParams} */ (
              this.params_)));
  }

  /**
   * Reads what the CMAF Header contributes to reconstruction: the track's ID,
   * the `trex` defaults every omitted field falls back to, and the `tenc`
   * per-sample IV size protected tracks need.
   *
   * shaka.util.Mp4BoxParsers.parseTREX is not used here because it reports
   * only two of the five values -- it exists for the transmuxer, which needs
   * no more -- and widening it would change what every other caller reads.
   *
   * @param {!Uint8Array} initData
   * @param {number} timescale
   * @return {?shaka.msf.LOCMAFParser.TrackParams}
   * @private
   */
  static readTrackParams_(initData, timescale) {
    const Mp4Parser = shaka.util.Mp4Parser;

    /** @type {?shaka.msf.LOCMAFParser.TrackParams} */
    let params = null;
    let isProtected = false;
    let defaultPerSampleIvSize = 0;

    new Mp4Parser()
        .box('moov', Mp4Parser.children)
        .box('mvex', Mp4Parser.children)
        .fullBox('trex', (box) => {
          const reader = box.reader;
          params = {
            trackId: reader.readUint32(),
            timescale,
            trexSampleDescriptionIndex: reader.readUint32(),
            trexSampleDuration: reader.readUint32(),
            trexSampleSize: reader.readUint32(),
            trexSampleFlags: reader.readUint32(),
            isProtected: false,
            defaultPerSampleIvSize: 0,
          };
        })
        .boxes([...Mp4Parser.SAMPLE_TABLE_PATH, 'sinf', 'schi'],
            Mp4Parser.children)
        .fullBox('stsd', Mp4Parser.sampleDescription)
        .box('encv', Mp4Parser.visualSampleEntry)
        .box('enca', Mp4Parser.audioSampleEntry)
        .fullBox('tenc', (box) => {
          goog.asserts.assert(
              box.version != null,
              'TENC is a full box and should have a valid version.');
          const parsed =
              shaka.util.Mp4BoxParsers.parseTENC(box.reader, box.version);
          isProtected = parsed.defaultIsProtected === 1;
          defaultPerSampleIvSize = parsed.defaultPerSampleIVSize;
        })
        .parse(initData, /* partialOkay= */ true);

    if (!params) {
      return null;
    }
    params.isProtected = isProtected;
    params.defaultPerSampleIvSize = defaultPerSampleIvSize;
    return params;
  }
};


/**
 * The LOCMAF packaging version this build reads, as carried in the catalog's
 * `locmafVersion` field.
 *
 * @const {string}
 */
shaka.msf.packaging.Locmaf.VERSION = '0.3';


/**
 * @implements {shaka.extern.MsfSegmenter}
 * @final
 */
shaka.msf.packaging.LocmafSegmenter = class {
  /**
   * @param {!shaka.msf.LOCMAFParser} parser
   */
  constructor(parser) {
    /** @private {!shaka.msf.LOCMAFParser} */
    this.parser_ = parser;
  }

  /**
   * @override
   */
  push(obj) {
    // An object with an empty payload carries a status rather than media, but
    // it still occupies an object ID, so the parser is told about it: a
    // status object between two chunks is not a gap in the delta chain.
    const chunk = this.parser_.parse(obj);
    if (!chunk) {
      return [];
    }

    return [{
      startTime: chunk.startTime,
      duration: chunk.duration,
      data: chunk.data,
      timestampOffset: 0,
      discontinuitySequence: -1,
    }];
  }
};


shaka.msf.PackagingRegistry.registerPackaging(
    'locmaf', () => new shaka.msf.packaging.Locmaf());