text: // Copyright (c) 2012 The Chromium Authors. All rights reserved.
  // Use of this source code is governed by a BSD-style license that can be
  // found in the LICENSE file.
  
  'use strict';
  
  /**
   * @param {MetadataDispatcher} parent Parent object.
   * @constructor
   */
  function MpegParser(parent) {
    MetadataParser.call(this, parent, 'mpeg', /\.(mp4|m4v|m4a|mpe?g4?)$/i);
    this.mimeType = 'video/mpeg';
  }
  
  MpegParser.prototype = {__proto__: MetadataParser.prototype};
  
  /**
   * Size of the atom header.
   */
  MpegParser.HEADER_SIZE = 8;
  
  /**
   * @param {ByteReader} br ByteReader instance.
   * @param {number=} opt_end End of atom position.
   * @return {number} Atom size.
   */
  MpegParser.readAtomSize = function(br, opt_end) {
    var pos = br.tell();
  
    if (opt_end) {
      // Assert that opt_end <= buffer end.
      // When supplied, opt_end is the end of the enclosing atom and is used to
      // check the correct nesting.
      br.validateRead(opt_end - pos);
    }
  
    var size = br.readScalar(4, false, opt_end);
  
    if (size < MpegParser.HEADER_SIZE)
      throw 'atom too short (' + size + ') @' + pos;
  
    if (opt_end && pos + size > opt_end)
      throw 'atom too long (' + size + '>' + (opt_end - pos) + ') @' + pos;
  
    return size;
  };
  
  /**
   * @param {ByteReader} br ByteReader instance.
   * @param {number=} opt_end End of atom position.
   * @return {string} Atom name.
   */
  MpegParser.readAtomName = function(br, opt_end) {
    return br.readString(4, opt_end).toLowerCase();
  };
  
  /**
   * @param {Object} metadata Metadata object.
   * @return {Object} Root of the parser tree.
   */
  MpegParser.createRootParser = function(metadata) {
    function findParentAtom(atom, name) {
      for (;;) {
        atom = atom.parent;
        if (!atom) return null;
        if (atom.name == name) return atom;
      }
    }
  
    function parseFtyp(br, atom) {
      metadata.brand = br.readString(4, atom.end);
    }
  
    function parseMvhd(br, atom) {
      var version = br.readScalar(4, false, atom.end);
      var offset = (version == 0) ? 8 : 16;
      br.seek(offset, ByteReader.SEEK_CUR);
      var timescale = br.readScalar(4, false, atom.end);
      var duration = br.readScalar(4, false, atom.end);
      metadata.duration = duration / timescale;
    }
  
    function parseHdlr(br, atom) {
      br.seek(8, ByteReader.SEEK_CUR);
      findParentAtom(atom, 'trak').trackType = br.readString(4, atom.end);
    }
  
    function parseStsd(br, atom) {
      var track = findParentAtom(atom, 'trak');
      if (track && track.trackType == 'vide') {
        br.seek(40, ByteReader.SEEK_CUR);
        metadata.width = br.readScalar(2, false, atom.end);
        metadata.height = br.readScalar(2, false, atom.end);
      }
    }
  
    function parseDataString(name, br, atom) {
      br.seek(8, ByteReader.SEEK_CUR);
      metadata[name] = br.readString(atom.end - br.tell(), atom.end);
    }
  
    function parseCovr(br, atom) {
      br.seek(8, ByteReader.SEEK_CUR);
      metadata.thumbnailURL = br.readImage(atom.end - br.tell(), atom.end);
    }
  
    // 'meta' atom can occur at one of the several places in the file structure.
    var parseMeta = {
      ilst: {
        '©nam': { data: parseDataString.bind(null, 'title') },
        '©alb': { data: parseDataString.bind(null, 'album') },
        '©art': { data: parseDataString.bind(null, 'artist') },
        'covr': { data: parseCovr }
      },
      versioned: true
    };
  
    // main parser for the entire file structure.
    return {
      ftyp: parseFtyp,
      moov: {
        mvhd: parseMvhd,
        trak: {
          mdia: {
            hdlr: parseHdlr,
            minf: {
              stbl: {
                stsd: parseStsd
              }
            }
          },
          meta: parseMeta
        },
        udta: {
          meta: parseMeta
        },
        meta: parseMeta
      },
      meta: parseMeta
    };
  };
  
  /**
   *
   * @param {File} file File.
   * @param {Object} metadata Metadata.
   * @param {function(Object)} callback Success callback.
   * @param {function} onError Error callback.
   */
  MpegParser.prototype.parse = function(file, metadata, callback, onError) {
    this.rootParser_ = MpegParser.createRootParser(metadata);
  
    // Kick off the processing by reading the first atom's header.
    this.requestRead(file, 0, MpegParser.HEADER_SIZE, null,
        onError, callback.bind(null, metadata));
  };
  
  /**
   * @param {function(ByteReader, Object)|Object} parser Parser tree node.
   * @param {ByteReader} br ByteReader instance.
   * @param {Object} atom Atom descriptor.
   * @param {number} filePos File position of the atom start.
   */
  MpegParser.prototype.applyParser = function(parser, br, atom, filePos) {
    if (this.verbose) {
      var path = atom.name;
      for (var p = atom.parent; p && p.name; p = p.parent) {
        path = p.name + '.' + path;
      }
  
      var action;
      if (!parser) {
        action = 'skipping ';
      } else if (parser instanceof Function) {
        action = 'parsing  ';
      } else {
        action = 'recursing';
      }
  
      var start = atom.start - MpegParser.HEADER_SIZE;
      this.vlog(path + ': ' +
                '@' + (filePos + start) + ':' + (atom.end - start),
                action);
    }
  
    if (parser) {
      if (parser instanceof Function) {
        br.pushSeek(atom.start);
        parser(br, atom);
        br.popSeek();
      } else {
        if (parser.versioned) {
          atom.start += 4;
        }
        this.parseMpegAtomsInRange(parser, br, atom, filePos);
      }
    }
  };
  
  /**
   * @param {function(ByteReader, Object)|Object} parser Parser tree node.
   * @param {ByteReader} br ByteReader instance.
   * @param {Object} parentAtom Parent atom descriptor.
   * @param {number} filePos File position of the atom start.
   */
  MpegParser.prototype.parseMpegAtomsInRange = function(
      parser, br, parentAtom, filePos) {
    var count = 0;
    for (var offset = parentAtom.start; offset != parentAtom.end;) {
      if (count++ > 100) // Most likely we are looping through a corrupt file.
        throw 'too many child atoms in ' + parentAtom.name + ' @' + offset;
  
      br.seek(offset);
      var size = MpegParser.readAtomSize(br, parentAtom.end);
      var name = MpegParser.readAtomName(br, parentAtom.end);
  
      this.applyParser(
          parser[name],
          br,
          { start: offset + MpegParser.HEADER_SIZE,
            end: offset + size,
            name: name,
            parent: parentAtom
          },
          filePos
      );
  
      offset += size;
    }
  };
  
  /**
   * @param {File} file File.
   * @param {number} filePos Start position in the file.
   * @param {number} size Atom size.
   * @param {string} name Atom name.
   * @param {function} onError Error callback.
   * @param {function} onSuccess Success callback.
   */
  MpegParser.prototype.requestRead = function(
      file, filePos, size, name, onError, onSuccess) {
    var self = this;
    var reader = new FileReader();
    reader.onerror = onError;
    reader.onload = function(event) {
      self.processTopLevelAtom(
          reader.result, file, filePos, size, name, onError, onSuccess);
    };
    this.vlog('reading @' + filePos + ':' + size);
    reader.readAsArrayBuffer(file.slice(filePos, filePos + size));
  };
  
  /**
   * @param {ArrayBuffer} buf Data buffer.
   * @param {File} file File.
   * @param {number} filePos Start position in the file.
   * @param {number} size Atom size.
   * @param {string} name Atom name.
   * @param {function} onError Error callback.
   * @param {function} onSuccess Success callback.
   */
  MpegParser.prototype.processTopLevelAtom = function(
      buf, file, filePos, size, name, onError, onSuccess) {
    try {
      var br = new ByteReader(buf);
  
      // the header has already been read.
      var atomEnd = size - MpegParser.HEADER_SIZE;
  
      var bufLength = buf.byteLength;
  
      // Check the available data size. It should be either exactly
      // what we requested or HEADER_SIZE bytes less (for the last atom).
      if (bufLength != atomEnd && bufLength != size) {
        throw 'Read failure @' + filePos + ', ' +
            'requested ' + size + ', read ' + bufLength;
      }
  
      // Process the top level atom.
      if (name) { // name is null only the first time.
        this.applyParser(
            this.rootParser_[name],
            br,
            {start: 0, end: atomEnd, name: name},
            filePos
        );
      }
  
      filePos += bufLength;
      if (bufLength == size) {
        // The previous read returned everything we asked for, including
        // the next atom header at the end of the buffer.
        // Parse this header and schedule the next read.
        br.seek(-MpegParser.HEADER_SIZE, ByteReader.SEEK_END);
        var nextSize = MpegParser.readAtomSize(br);
        var nextName = MpegParser.readAtomName(br);
  
        // If we do not have a parser for the next atom, skip the content and
        // read only the header (the one after the next).
        if (!this.rootParser_[nextName]) {
          filePos += nextSize - MpegParser.HEADER_SIZE;
          nextSize = MpegParser.HEADER_SIZE;
        }
  
        this.requestRead(file, filePos, nextSize, nextName, onError, onSuccess);
      } else {
        // The previous read did not return the next atom header, EOF reached.
        this.vlog('EOF @' + filePos);
        onSuccess();
      }
    } catch (e) {
      onError(e.toString());
    }
  };
  
  MetadataDispatcher.registerParserClass(MpegParser);
  