"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.parseMediaInfo = exports.parseTracks = exports.addSampleDescription = exports.buildFrameTable = exports.findNamedBox = exports.findBox = exports.parseDescriptors = void 0;

var _byteHelpers = require("./byte-helpers.js");

var _codecHelpers = require("./codec-helpers.js");

var _opusHelpers = require("./opus-helpers.js");

var normalizePath = function normalizePath(path) {
  if (typeof path === 'string') {
    return (0, _byteHelpers.stringToBytes)(path);
  }

  if (typeof path === 'number') {
    return path;
  }

  return path;
};

var normalizePaths = function normalizePaths(paths) {
  if (!Array.isArray(paths)) {
    return [normalizePath(paths)];
  }

  return paths.map(function (p) {
    return normalizePath(p);
  });
};

var DESCRIPTORS;

var parseDescriptors = function parseDescriptors(bytes) {
  bytes = (0, _byteHelpers.toUint8)(bytes);
  var results = [];
  var i = 0;

  while (bytes.length > i) {
    var tag = bytes[i];
    var size = 0;
    var headerSize = 0; // tag

    headerSize++;
    var byte = bytes[headerSize]; // first byte

    headerSize++;

    while (byte & 0x80) {
      size = (byte & 0x7F) << 7;
      byte = bytes[headerSize];
      headerSize++;
    }

    size += byte & 0x7F;

    for (var z = 0; z < DESCRIPTORS.length; z++) {
      var _DESCRIPTORS$z = DESCRIPTORS[z],
          id = _DESCRIPTORS$z.id,
          parser = _DESCRIPTORS$z.parser;

      if (tag === id) {
        results.push(parser(bytes.subarray(headerSize, headerSize + size)));
        break;
      }
    }

    i += size + headerSize;
  }

  return results;
};

exports.parseDescriptors = parseDescriptors;
DESCRIPTORS = [{
  id: 0x03,
  parser: function parser(bytes) {
    var desc = {
      tag: 0x03,
      id: bytes[0] << 8 | bytes[1],
      flags: bytes[2],
      size: 3,
      dependsOnEsId: 0,
      ocrEsId: 0,
      descriptors: [],
      url: ''
    }; // depends on es id

    if (desc.flags & 0x80) {
      desc.dependsOnEsId = bytes[desc.size] << 8 | bytes[desc.size + 1];
      desc.size += 2;
    } // url


    if (desc.flags & 0x40) {
      var len = bytes[desc.size];
      desc.url = (0, _byteHelpers.bytesToString)(bytes.subarray(desc.size + 1, desc.size + 1 + len));
      desc.size += len;
    } // ocr es id


    if (desc.flags & 0x20) {
      desc.ocrEsId = bytes[desc.size] << 8 | bytes[desc.size + 1];
      desc.size += 2;
    }

    desc.descriptors = parseDescriptors(bytes.subarray(desc.size)) || [];
    return desc;
  }
}, {
  id: 0x04,
  parser: function parser(bytes) {
    // DecoderConfigDescriptor
    var desc = {
      tag: 0x04,
      oti: bytes[0],
      streamType: bytes[1],
      bufferSize: bytes[2] << 16 | bytes[3] << 8 | bytes[4],
      maxBitrate: bytes[5] << 24 | bytes[6] << 16 | bytes[7] << 8 | bytes[8],
      avgBitrate: bytes[9] << 24 | bytes[10] << 16 | bytes[11] << 8 | bytes[12],
      descriptors: parseDescriptors(bytes.subarray(13))
    };
    return desc;
  }
}, {
  id: 0x05,
  parser: function parser(bytes) {
    // DecoderSpecificInfo
    return {
      tag: 0x05,
      bytes: bytes
    };
  }
}, {
  id: 0x06,
  parser: function parser(bytes) {
    // SLConfigDescriptor
    return {
      tag: 0x06,
      bytes: bytes
    };
  }
}];
/**
 * find any number of boxes by name given a path to it in an iso bmff
 * such as mp4.
 *
 * @param {TypedArray} bytes
 *        bytes for the iso bmff to search for boxes in
 *
 * @param {Uint8Array[]|string[]|string|Uint8Array} name
 *        An array of paths or a single path representing the name
 *        of boxes to search through in bytes. Paths may be
 *        uint8 (character codes) or strings.
 *
 * @param {boolean} [complete=false]
 *        Should we search only for complete boxes on the final path.
 *        This is very useful when you do not want to get back partial boxes
 *        in the case of streaming files.
 *
 * @return {Uint8Array[]}
 *         An array of the end paths that we found.
 */

var findBox = function findBox(bytes, paths, complete) {
  if (complete === void 0) {
    complete = false;
  }

  paths = normalizePaths(paths);
  bytes = (0, _byteHelpers.toUint8)(bytes);
  var results = [];

  if (!paths.length) {
    // short-circuit the search for empty paths
    return results;
  }

  var i = 0;

  while (i < bytes.length) {
    var size = (bytes[i] << 24 | bytes[i + 1] << 16 | bytes[i + 2] << 8 | bytes[i + 3]) >>> 0;
    var type = bytes.subarray(i + 4, i + 8); // invalid box format.

    if (size === 0) {
      break;
    }

    var end = i + size;

    if (end > bytes.length) {
      // this box is bigger than the number of bytes we have
      // and complete is set, we cannot find any more boxes.
      if (complete) {
        break;
      }

      end = bytes.length;
    }

    var data = bytes.subarray(i + 8, end);

    if ((0, _byteHelpers.bytesMatch)(type, paths[0])) {
      if (paths.length === 1) {
        // this is the end of the path and we've found the box we were
        // looking for
        results.push(data);
      } else {
        // recursively search for the next box along the path
        results.push.apply(results, findBox(data, paths.slice(1), complete));
      }
    }

    i = end;
  } // we've finished searching all of bytes


  return results;
};
/**
 * Search for a single matching box by name in an iso bmff format like
 * mp4. This function is useful for finding codec boxes which
 * can be placed arbitrarily in sample descriptions depending
 * on the version of the file or file type.
 *
 * @param {TypedArray} bytes
 *        bytes for the iso bmff to search for boxes in
 *
 * @param {string|Uint8Array} name
 *        The name of the box to find.
 *
 * @return {Uint8Array[]}
 *         a subarray of bytes representing the name boxed we found.
 */


exports.findBox = findBox;

var findNamedBox = function findNamedBox(bytes, name) {
  name = normalizePath(name);

  if (!name.length) {
    // short-circuit the search for empty paths
    return bytes.subarray(bytes.length);
  }

  var i = 0;

  while (i < bytes.length) {
    if ((0, _byteHelpers.bytesMatch)(bytes.subarray(i, i + name.length), name)) {
      var size = (bytes[i - 4] << 24 | bytes[i - 3] << 16 | bytes[i - 2] << 8 | bytes[i - 1]) >>> 0;
      var end = size > 1 ? i + size : bytes.byteLength;
      return bytes.subarray(i + 4, end);
    }

    i++;
  } // we've finished searching all of bytes


  return bytes.subarray(bytes.length);
};

exports.findNamedBox = findNamedBox;

var parseSamples = function parseSamples(data, entrySize, parseEntry) {
  if (entrySize === void 0) {
    entrySize = 4;
  }

  if (parseEntry === void 0) {
    parseEntry = function parseEntry(d) {
      return (0, _byteHelpers.bytesToNumber)(d);
    };
  }

  var entries = [];

  if (!data || !data.length) {
    return entries;
  }

  var entryCount = (0, _byteHelpers.bytesToNumber)(data.subarray(4, 8));

  for (var i = 8; entryCount; i += entrySize, entryCount--) {
    entries.push(parseEntry(data.subarray(i, i + entrySize)));
  }

  return entries;
};

var buildFrameTable = function buildFrameTable(stbl, timescale) {
  var keySamples = parseSamples(findBox(stbl, ['stss'])[0]);
  var chunkOffsets = parseSamples(findBox(stbl, ['stco'])[0]);
  var timeToSamples = parseSamples(findBox(stbl, ['stts'])[0], 8, function (entry) {
    return {
      sampleCount: (0, _byteHelpers.bytesToNumber)(entry.subarray(0, 4)),
      sampleDelta: (0, _byteHelpers.bytesToNumber)(entry.subarray(4, 8))
    };
  });
  var samplesToChunks = parseSamples(findBox(stbl, ['stsc'])[0], 12, function (entry) {
    return {
      firstChunk: (0, _byteHelpers.bytesToNumber)(entry.subarray(0, 4)),
      samplesPerChunk: (0, _byteHelpers.bytesToNumber)(entry.subarray(4, 8)),
      sampleDescriptionIndex: (0, _byteHelpers.bytesToNumber)(entry.subarray(8, 12))
    };
  });
  var stsz = findBox(stbl, ['stsz'])[0]; // stsz starts with a 4 byte sampleSize which we don't need

  var sampleSizes = parseSamples(stsz && stsz.length && stsz.subarray(4) || null);
  var frames = [];

  for (var chunkIndex = 0; chunkIndex < chunkOffsets.length; chunkIndex++) {
    var samplesInChunk = void 0;

    for (var i = 0; i < samplesToChunks.length; i++) {
      var sampleToChunk = samplesToChunks[i];
      var isThisOne = chunkIndex + 1 >= sampleToChunk.firstChunk && (i + 1 >= samplesToChunks.length || chunkIndex + 1 < samplesToChunks[i + 1].firstChunk);

      if (isThisOne) {
        samplesInChunk = sampleToChunk.samplesPerChunk;
        break;
      }
    }

    var chunkOffset = chunkOffsets[chunkIndex];

    for (var _i = 0; _i < samplesInChunk; _i++) {
      var frameEnd = sampleSizes[frames.length]; // if we don't have key samples every frame is a keyframe

      var keyframe = !keySamples.length;

      if (keySamples.length && keySamples.indexOf(frames.length + 1) !== -1) {
        keyframe = true;
      }

      var frame = {
        keyframe: keyframe,
        start: chunkOffset,
        end: chunkOffset + frameEnd
      };

      for (var k = 0; k < timeToSamples.length; k++) {
        var _timeToSamples$k = timeToSamples[k],
            sampleCount = _timeToSamples$k.sampleCount,
            sampleDelta = _timeToSamples$k.sampleDelta;

        if (frames.length <= sampleCount) {
          // ms to ns
          var lastTimestamp = frames.length ? frames[frames.length - 1].timestamp : 0;
          frame.timestamp = lastTimestamp + sampleDelta / timescale * 1000;
          frame.duration = sampleDelta;
          break;
        }
      }

      frames.push(frame);
      chunkOffset += frameEnd;
    }
  }

  return frames;
};

exports.buildFrameTable = buildFrameTable;

var addSampleDescription = function addSampleDescription(track, bytes) {
  var codec = (0, _byteHelpers.bytesToString)(bytes.subarray(0, 4));

  if (track.type === 'video') {
    track.info = track.info || {};
    track.info.width = bytes[28] << 8 | bytes[29];
    track.info.height = bytes[30] << 8 | bytes[31];
  } else if (track.type === 'audio') {
    track.info = track.info || {};
    track.info.channels = bytes[20] << 8 | bytes[21];
    track.info.bitDepth = bytes[22] << 8 | bytes[23];
    track.info.sampleRate = bytes[28] << 8 | bytes[29];
  }

  if (codec === 'avc1') {
    var avcC = findNamedBox(bytes, 'avcC'); // AVCDecoderConfigurationRecord

    codec += "." + (0, _codecHelpers.getAvcCodec)(avcC);
    track.info.avcC = avcC; // TODO: do we need to parse all this?

    /* {
      configurationVersion: avcC[0],
      profile: avcC[1],
      profileCompatibility: avcC[2],
      level: avcC[3],
      lengthSizeMinusOne: avcC[4] & 0x3
    };
     let spsNalUnitCount = avcC[5] & 0x1F;
    const spsNalUnits = track.info.avc.spsNalUnits = [];
     // past spsNalUnitCount
    let offset = 6;
     while (spsNalUnitCount--) {
      const nalLen = avcC[offset] << 8 | avcC[offset + 1];
       spsNalUnits.push(avcC.subarray(offset + 2, offset + 2 + nalLen));
       offset += nalLen + 2;
    }
    let ppsNalUnitCount = avcC[offset];
    const ppsNalUnits = track.info.avc.ppsNalUnits = [];
     // past ppsNalUnitCount
    offset += 1;
     while (ppsNalUnitCount--) {
      const nalLen = avcC[offset] << 8 | avcC[offset + 1];
       ppsNalUnits.push(avcC.subarray(offset + 2, offset + 2 + nalLen));
       offset += nalLen + 2;
    }*/
    // HEVCDecoderConfigurationRecord
  } else if (codec === 'hvc1' || codec === 'hev1') {
    codec += "." + (0, _codecHelpers.getHvcCodec)(findNamedBox(bytes, 'hvcC'));
  } else if (codec === 'mp4a' || codec === 'mp4v') {
    var esds = findNamedBox(bytes, 'esds');
    var esDescriptor = parseDescriptors(esds.subarray(4))[0];
    var decoderConfig = esDescriptor && esDescriptor.descriptors.filter(function (_ref) {
      var tag = _ref.tag;
      return tag === 0x04;
    })[0];

    if (decoderConfig) {
      // most codecs do not have a further '.'
      // such as 0xa5 for ac-3 and 0xa6 for e-ac-3
      codec += '.' + (0, _byteHelpers.toHexString)(decoderConfig.oti);

      if (decoderConfig.oti === 0x40) {
        codec += '.' + (decoderConfig.descriptors[0].bytes[0] >> 3).toString();
      } else if (decoderConfig.oti === 0x20) {
        codec += '.' + decoderConfig.descriptors[0].bytes[4].toString();
      } else if (decoderConfig.oti === 0xdd) {
        codec = 'vorbis';
      }
    } else if (track.type === 'audio') {
      codec += '.40.2';
    } else {
      codec += '.20.9';
    }
  } else if (codec === 'av01') {
    // AV1DecoderConfigurationRecord
    codec += "." + (0, _codecHelpers.getAv1Codec)(findNamedBox(bytes, 'av1C'));
  } else if (codec === 'vp09') {
    // VPCodecConfigurationRecord
    var vpcC = findNamedBox(bytes, 'vpcC'); // https://www.webmproject.org/vp9/mp4/

    var profile = vpcC[0];
    var level = vpcC[1];
    var bitDepth = vpcC[2] >> 4;
    var chromaSubsampling = (vpcC[2] & 0x0F) >> 1;
    var videoFullRangeFlag = (vpcC[2] & 0x0F) >> 3;
    var colourPrimaries = vpcC[3];
    var transferCharacteristics = vpcC[4];
    var matrixCoefficients = vpcC[5];
    codec += "." + (0, _byteHelpers.padStart)(profile, 2, '0');
    codec += "." + (0, _byteHelpers.padStart)(level, 2, '0');
    codec += "." + (0, _byteHelpers.padStart)(bitDepth, 2, '0');
    codec += "." + (0, _byteHelpers.padStart)(chromaSubsampling, 2, '0');
    codec += "." + (0, _byteHelpers.padStart)(colourPrimaries, 2, '0');
    codec += "." + (0, _byteHelpers.padStart)(transferCharacteristics, 2, '0');
    codec += "." + (0, _byteHelpers.padStart)(matrixCoefficients, 2, '0');
    codec += "." + (0, _byteHelpers.padStart)(videoFullRangeFlag, 2, '0');
  } else if (codec === 'theo') {
    codec = 'theora';
  } else if (codec === 'spex') {
    codec = 'speex';
  } else if (codec === '.mp3') {
    codec = 'mp4a.40.34';
  } else if (codec === 'msVo') {
    codec = 'vorbis';
  } else if (codec === 'Opus') {
    codec = 'opus';
    var dOps = findNamedBox(bytes, 'dOps');
    track.info.opus = (0, _opusHelpers.parseOpusHead)(dOps); // TODO: should this go into the webm code??
    // Firefox requires a codecDelay for opus playback
    // see https://bugzilla.mozilla.org/show_bug.cgi?id=1276238

    track.info.codecDelay = 6500000;
  } else {
    codec = codec.toLowerCase();
  }
  /* eslint-enable */
  // flac, ac-3, ec-3, opus


  track.codec = codec;
};

exports.addSampleDescription = addSampleDescription;

var parseTracks = function parseTracks(bytes, frameTable) {
  if (frameTable === void 0) {
    frameTable = true;
  }

  bytes = (0, _byteHelpers.toUint8)(bytes);
  var traks = findBox(bytes, ['moov', 'trak'], true);
  var tracks = [];
  traks.forEach(function (trak) {
    var track = {
      bytes: trak
    };
    var mdia = findBox(trak, ['mdia'])[0];
    var hdlr = findBox(mdia, ['hdlr'])[0];
    var trakType = (0, _byteHelpers.bytesToString)(hdlr.subarray(8, 12));

    if (trakType === 'soun') {
      track.type = 'audio';
    } else if (trakType === 'vide') {
      track.type = 'video';
    } else {
      track.type = trakType;
    }

    var tkhd = findBox(trak, ['tkhd'])[0];

    if (tkhd) {
      var view = new DataView(tkhd.buffer, tkhd.byteOffset, tkhd.byteLength);
      var tkhdVersion = view.getUint8(0);
      track.number = tkhdVersion === 0 ? view.getUint32(12) : view.getUint32(20);
    }

    var mdhd = findBox(mdia, ['mdhd'])[0];

    if (mdhd) {
      // mdhd is a FullBox, meaning it will have its own version as the first byte
      var version = mdhd[0];
      var index = version === 0 ? 12 : 20;
      track.timescale = (mdhd[index] << 24 | mdhd[index + 1] << 16 | mdhd[index + 2] << 8 | mdhd[index + 3]) >>> 0;
    }

    var stbl = findBox(mdia, ['minf', 'stbl'])[0];
    var stsd = findBox(stbl, ['stsd'])[0];
    var descriptionCount = (0, _byteHelpers.bytesToNumber)(stsd.subarray(4, 8));
    var offset = 8; // add codec and codec info

    while (descriptionCount--) {
      var len = (0, _byteHelpers.bytesToNumber)(stsd.subarray(offset, offset + 4));
      var sampleDescriptor = stsd.subarray(offset + 4, offset + 4 + len);
      addSampleDescription(track, sampleDescriptor);
      offset += 4 + len;
    }

    if (frameTable) {
      track.frameTable = buildFrameTable(stbl, track.timescale);
    } // codec has no sub parameters


    tracks.push(track);
  });
  return tracks;
};

exports.parseTracks = parseTracks;

var parseMediaInfo = function parseMediaInfo(bytes) {
  var mvhd = findBox(bytes, ['moov', 'mvhd'], true)[0];

  if (!mvhd || !mvhd.length) {
    return;
  }

  var info = {}; // ms to ns
  // mvhd v1 has 8 byte duration and other fields too

  if (mvhd[0] === 1) {
    info.timestampScale = (0, _byteHelpers.bytesToNumber)(mvhd.subarray(20, 24));
    info.duration = (0, _byteHelpers.bytesToNumber)(mvhd.subarray(24, 32));
  } else {
    info.timestampScale = (0, _byteHelpers.bytesToNumber)(mvhd.subarray(12, 16));
    info.duration = (0, _byteHelpers.bytesToNumber)(mvhd.subarray(16, 20));
  }

  info.bytes = mvhd;
  return info;
};

exports.parseMediaInfo = parseMediaInfo;