1// Copyright (c) 2012 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5'use strict';
6
7/**
8 * @param {MetadataDispatcher} parent Parent object.
9 * @constructor
10 */
11function MpegParser(parent) {
12  MetadataParser.call(this, parent, 'mpeg', /\.(mp4|m4v|m4a|mpe?g4?)$/i);
13  this.mimeType = 'video/mpeg';
14}
15
16MpegParser.prototype = {__proto__: MetadataParser.prototype};
17
18/**
19 * Size of the atom header.
20 */
21MpegParser.HEADER_SIZE = 8;
22
23/**
24 * @param {ByteReader} br ByteReader instance.
25 * @param {number=} opt_end End of atom position.
26 * @return {number} Atom size.
27 */
28MpegParser.readAtomSize = function(br, opt_end) {
29  var pos = br.tell();
30
31  if (opt_end) {
32    // Assert that opt_end <= buffer end.
33    // When supplied, opt_end is the end of the enclosing atom and is used to
34    // check the correct nesting.
35    br.validateRead(opt_end - pos);
36  }
37
38  var size = br.readScalar(4, false, opt_end);
39
40  if (size < MpegParser.HEADER_SIZE)
41    throw 'atom too short (' + size + ') @' + pos;
42
43  if (opt_end && pos + size > opt_end)
44    throw 'atom too long (' + size + '>' + (opt_end - pos) + ') @' + pos;
45
46  return size;
47};
48
49/**
50 * @param {ByteReader} br ByteReader instance.
51 * @param {number=} opt_end End of atom position.
52 * @return {string} Atom name.
53 */
54MpegParser.readAtomName = function(br, opt_end) {
55  return br.readString(4, opt_end).toLowerCase();
56};
57
58/**
59 * @param {Object} metadata Metadata object.
60 * @return {Object} Root of the parser tree.
61 */
62MpegParser.createRootParser = function(metadata) {
63  function findParentAtom(atom, name) {
64    for (;;) {
65      atom = atom.parent;
66      if (!atom) return null;
67      if (atom.name == name) return atom;
68    }
69  }
70
71  function parseFtyp(br, atom) {
72    metadata.brand = br.readString(4, atom.end);
73  }
74
75  function parseMvhd(br, atom) {
76    var version = br.readScalar(4, false, atom.end);
77    var offset = (version == 0) ? 8 : 16;
78    br.seek(offset, ByteReader.SEEK_CUR);
79    var timescale = br.readScalar(4, false, atom.end);
80    var duration = br.readScalar(4, false, atom.end);
81    metadata.duration = duration / timescale;
82  }
83
84  function parseHdlr(br, atom) {
85    br.seek(8, ByteReader.SEEK_CUR);
86    findParentAtom(atom, 'trak').trackType = br.readString(4, atom.end);
87  }
88
89  function parseStsd(br, atom) {
90    var track = findParentAtom(atom, 'trak');
91    if (track && track.trackType == 'vide') {
92      br.seek(40, ByteReader.SEEK_CUR);
93      metadata.width = br.readScalar(2, false, atom.end);
94      metadata.height = br.readScalar(2, false, atom.end);
95    }
96  }
97
98  function parseDataString(name, br, atom) {
99    br.seek(8, ByteReader.SEEK_CUR);
100    metadata[name] = br.readString(atom.end - br.tell(), atom.end);
101  }
102
103  function parseCovr(br, atom) {
104    br.seek(8, ByteReader.SEEK_CUR);
105    metadata.thumbnailURL = br.readImage(atom.end - br.tell(), atom.end);
106  }
107
108  // 'meta' atom can occur at one of the several places in the file structure.
109  var parseMeta = {
110    ilst: {
111      '©nam': { data: parseDataString.bind(null, 'title') },
112      '©alb': { data: parseDataString.bind(null, 'album') },
113      '©art': { data: parseDataString.bind(null, 'artist') },
114      'covr': { data: parseCovr }
115    },
116    versioned: true
117  };
118
119  // main parser for the entire file structure.
120  return {
121    ftyp: parseFtyp,
122    moov: {
123      mvhd: parseMvhd,
124      trak: {
125        mdia: {
126          hdlr: parseHdlr,
127          minf: {
128            stbl: {
129              stsd: parseStsd
130            }
131          }
132        },
133        meta: parseMeta
134      },
135      udta: {
136        meta: parseMeta
137      },
138      meta: parseMeta
139    },
140    meta: parseMeta
141  };
142};
143
144/**
145 *
146 * @param {File} file File.
147 * @param {Object} metadata Metadata.
148 * @param {function(Object)} callback Success callback.
149 * @param {function} onError Error callback.
150 */
151MpegParser.prototype.parse = function(file, metadata, callback, onError) {
152  this.rootParser_ = MpegParser.createRootParser(metadata);
153
154  // Kick off the processing by reading the first atom's header.
155  this.requestRead(file, 0, MpegParser.HEADER_SIZE, null,
156      onError, callback.bind(null, metadata));
157};
158
159/**
160 * @param {function(ByteReader, Object)|Object} parser Parser tree node.
161 * @param {ByteReader} br ByteReader instance.
162 * @param {Object} atom Atom descriptor.
163 * @param {number} filePos File position of the atom start.
164 */
165MpegParser.prototype.applyParser = function(parser, br, atom, filePos) {
166  if (this.verbose) {
167    var path = atom.name;
168    for (var p = atom.parent; p && p.name; p = p.parent) {
169      path = p.name + '.' + path;
170    }
171
172    var action;
173    if (!parser) {
174      action = 'skipping ';
175    } else if (parser instanceof Function) {
176      action = 'parsing  ';
177    } else {
178      action = 'recursing';
179    }
180
181    var start = atom.start - MpegParser.HEADER_SIZE;
182    this.vlog(path + ': ' +
183              '@' + (filePos + start) + ':' + (atom.end - start),
184              action);
185  }
186
187  if (parser) {
188    if (parser instanceof Function) {
189      br.pushSeek(atom.start);
190      parser(br, atom);
191      br.popSeek();
192    } else {
193      if (parser.versioned) {
194        atom.start += 4;
195      }
196      this.parseMpegAtomsInRange(parser, br, atom, filePos);
197    }
198  }
199};
200
201/**
202 * @param {function(ByteReader, Object)|Object} parser Parser tree node.
203 * @param {ByteReader} br ByteReader instance.
204 * @param {Object} parentAtom Parent atom descriptor.
205 * @param {number} filePos File position of the atom start.
206 */
207MpegParser.prototype.parseMpegAtomsInRange = function(
208    parser, br, parentAtom, filePos) {
209  var count = 0;
210  for (var offset = parentAtom.start; offset != parentAtom.end;) {
211    if (count++ > 100) // Most likely we are looping through a corrupt file.
212      throw 'too many child atoms in ' + parentAtom.name + ' @' + offset;
213
214    br.seek(offset);
215    var size = MpegParser.readAtomSize(br, parentAtom.end);
216    var name = MpegParser.readAtomName(br, parentAtom.end);
217
218    this.applyParser(
219        parser[name],
220        br,
221        { start: offset + MpegParser.HEADER_SIZE,
222          end: offset + size,
223          name: name,
224          parent: parentAtom
225        },
226        filePos
227    );
228
229    offset += size;
230  }
231};
232
233/**
234 * @param {File} file File.
235 * @param {number} filePos Start position in the file.
236 * @param {number} size Atom size.
237 * @param {string} name Atom name.
238 * @param {function} onError Error callback.
239 * @param {function} onSuccess Success callback.
240 */
241MpegParser.prototype.requestRead = function(
242    file, filePos, size, name, onError, onSuccess) {
243  var self = this;
244  var reader = new FileReader();
245  reader.onerror = onError;
246  reader.onload = function(event) {
247    self.processTopLevelAtom(
248        reader.result, file, filePos, size, name, onError, onSuccess);
249  };
250  this.vlog('reading @' + filePos + ':' + size);
251  reader.readAsArrayBuffer(file.slice(filePos, filePos + size));
252};
253
254/**
255 * @param {ArrayBuffer} buf Data buffer.
256 * @param {File} file File.
257 * @param {number} filePos Start position in the file.
258 * @param {number} size Atom size.
259 * @param {string} name Atom name.
260 * @param {function} onError Error callback.
261 * @param {function} onSuccess Success callback.
262 */
263MpegParser.prototype.processTopLevelAtom = function(
264    buf, file, filePos, size, name, onError, onSuccess) {
265  try {
266    var br = new ByteReader(buf);
267
268    // the header has already been read.
269    var atomEnd = size - MpegParser.HEADER_SIZE;
270
271    var bufLength = buf.byteLength;
272
273    // Check the available data size. It should be either exactly
274    // what we requested or HEADER_SIZE bytes less (for the last atom).
275    if (bufLength != atomEnd && bufLength != size) {
276      throw 'Read failure @' + filePos + ', ' +
277          'requested ' + size + ', read ' + bufLength;
278    }
279
280    // Process the top level atom.
281    if (name) { // name is null only the first time.
282      this.applyParser(
283          this.rootParser_[name],
284          br,
285          {start: 0, end: atomEnd, name: name},
286          filePos
287      );
288    }
289
290    filePos += bufLength;
291    if (bufLength == size) {
292      // The previous read returned everything we asked for, including
293      // the next atom header at the end of the buffer.
294      // Parse this header and schedule the next read.
295      br.seek(-MpegParser.HEADER_SIZE, ByteReader.SEEK_END);
296      var nextSize = MpegParser.readAtomSize(br);
297      var nextName = MpegParser.readAtomName(br);
298
299      // If we do not have a parser for the next atom, skip the content and
300      // read only the header (the one after the next).
301      if (!this.rootParser_[nextName]) {
302        filePos += nextSize - MpegParser.HEADER_SIZE;
303        nextSize = MpegParser.HEADER_SIZE;
304      }
305
306      this.requestRead(file, filePos, nextSize, nextName, onError, onSuccess);
307    } else {
308      // The previous read did not return the next atom header, EOF reached.
309      this.vlog('EOF @' + filePos);
310      onSuccess();
311    }
312  } catch (e) {
313    onError(e.toString());
314  }
315};
316
317MetadataDispatcher.registerParserClass(MpegParser);
318