Source: lib/media/mp4_vtt_parser.js

/**
 * @license
 * Copyright 2016 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

goog.provide('shaka.media.Mp4VttParser');

goog.require('goog.asserts');
goog.require('shaka.log');
goog.require('shaka.media.TextEngine');
goog.require('shaka.media.VttTextParser');
goog.require('shaka.util.DataViewReader');
goog.require('shaka.util.Error');
goog.require('shaka.util.Mp4Parser');
goog.require('shaka.util.StringUtils');
goog.require('shaka.util.TextParser');


/**
 * @namespace
 * @summary Extracts a VTT segment from an MP4 file and maps it to cue objects.
 * @param {ArrayBuffer} data
 * @param {number} offset
 * @param {?number} segmentStartTime
 * @param {?number} segmentEndTime
 * @param {boolean} useRelativeCueTimestamps Only used by the VTT parser
 * @return {!Array.<!TextTrackCue>}
 * @export
 */
shaka.media.Mp4VttParser =
    function(data, offset, segmentStartTime,
             segmentEndTime, useRelativeCueTimestamps) {
  var reader = new shaka.util.DataViewReader(
      new DataView(data),
      shaka.util.DataViewReader.Endianness.BIG_ENDIAN);
  var boxSize = shaka.util.Mp4Parser.findBox(
      shaka.util.Mp4Parser.BOX_TYPE_MDAT, reader);
  if (boxSize != shaka.util.Mp4Parser.BOX_NOT_FOUND) {
    // mdat box found, parse the content
    // valid media segment should have start and end time
    goog.asserts.assert(
        segmentStartTime != null, 'start time should not be null');
    goog.asserts.assert(segmentEndTime != null, 'end time should not be null');
    return shaka.media.Mp4VttParser.parseData_(
        reader.readBytes(boxSize - 8).buffer, offset,
        segmentStartTime, segmentEndTime);
  }
  var wvttBoxSize = shaka.util.Mp4Parser.findSampleDescriptionBox(
      data, shaka.media.Mp4VttParser.BOX_TYPE_WVTT);
  if (wvttBoxSize != shaka.util.Mp4Parser.BOX_NOT_FOUND) {
    // a valid vtt init segment, no actual subtitles yet
    return [];
  } else {
    throw new shaka.util.Error(
        shaka.util.Error.Category.TEXT,
        shaka.util.Error.Code.INVALID_MP4_VTT);
  }
};


/**
 * Parses the content of the mdat MP4 box into cue objects.
 *
 * @param {ArrayBuffer} data
 * @param {number} offset
 * @param {number} segmentStartTime
 * @param {number} segmentEndTime
 * @return {!Array.<!TextTrackCue>}
 * @private
 */
shaka.media.Mp4VttParser.parseData_ = function(
    data, offset, segmentStartTime, segmentEndTime) {
  var reader = new shaka.util.DataViewReader(
      new DataView(data),
      shaka.util.DataViewReader.Endianness.BIG_ENDIAN);

  segmentStartTime += offset;
  segmentEndTime += offset;

  var result = [];
  // Cues are represented as vttc boxes. Each box corresponds to a cue.
  while (reader.hasMoreData()) {
    var boxSize = shaka.util.Mp4Parser.findBox(
        shaka.media.Mp4VttParser.BOX_TYPE_VTTC, reader);
    if (boxSize == shaka.util.Mp4Parser.BOX_NOT_FOUND) {
      // No more cues
      break;
    }
    var cue = shaka.media.Mp4VttParser.parseCue_(
        reader.readBytes(boxSize - 8).buffer,
        segmentStartTime, segmentEndTime);
    if (cue)
      result.push(cue);
  }

  return result;
};


/**
 * Parses a vttc box into a cue.
 *
 * @param {ArrayBuffer} data
 * @param {number} segmentStartTime
 * @param {number} segmentEndTime
 * @return {TextTrackCue}
 * @private
 */
shaka.media.Mp4VttParser.parseCue_ = function(
    data, segmentStartTime, segmentEndTime) {
  var reader = new shaka.util.DataViewReader(
      new DataView(data),
      shaka.util.DataViewReader.Endianness.BIG_ENDIAN);

  var payload;
  var settings;
  var id;

  while (reader.hasMoreData()) {
    var startPosition = reader.getPosition();
    var size = reader.readUint32();
    var type = reader.readUint32();
    var content = shaka.util.StringUtils.fromUTF8(
        reader.readBytes(size - 8).buffer);
    if (size == 1) {
      size = reader.readUint64();
    } else if (size == 0) {
      size = reader.getLength() - startPosition;
    }

    switch (type) {
      case shaka.media.Mp4VttParser.BOX_TYPE_PAYL:
        payload = content;
        break;
      case shaka.media.Mp4VttParser.BOX_TYPE_IDEN:
        id = content;
        break;
      case shaka.media.Mp4VttParser.BOX_TYPE_STTG:
        settings = content;
        break;
      default:
        break;
    }
  }
  // payload box is mandatory
  if (!payload) {
    throw new shaka.util.Error(
        shaka.util.Error.Category.TEXT,
        shaka.util.Error.Code.INVALID_MP4_VTT);
  }

  var cue = shaka.media.TextEngine.makeCue(
      segmentStartTime, segmentEndTime, payload);
  if (!cue)
    return null;

  if (id)
    cue.id = id;
  if (settings) {
    var parser = new shaka.util.TextParser(settings);
    var word = parser.readWord();
    while (word) {
      if (!shaka.media.VttTextParser.parseSetting(cue, word)) {
        shaka.log.warning('VTT parser encountered an invalid VTT setting: ',
                          word,
                          ' The setting will be ignored.');
      }
      parser.skipWhitespace();
      word = parser.readWord();
    }
  }

  return cue;
};


/** @const {number} */
shaka.media.Mp4VttParser.BOX_TYPE_WVTT = 0x77767474;


/** @const {number} */
shaka.media.Mp4VttParser.BOX_TYPE_VTTC = 0x76747463;


/** @const {number} */
shaka.media.Mp4VttParser.BOX_TYPE_PAYL = 0x7061796C;


/** @const {number} */
shaka.media.Mp4VttParser.BOX_TYPE_IDEN = 0x6964656F;


/** @const {number} */
shaka.media.Mp4VttParser.BOX_TYPE_STTG = 0x73747467;


shaka.media.TextEngine.registerParser(
    'application/mp4; codecs="wvtt"', shaka.media.Mp4VttParser);