OLD | NEW |
| (Empty) |
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 */ | |
11 function MpegParser(parent) { | |
12 MetadataParser.call(this, parent, 'mpeg', /\.(mp4|m4v|m4a|mpe?g4?)$/i); | |
13 this.mimeType = 'video/mpeg'; | |
14 } | |
15 | |
16 MpegParser.prototype = {__proto__: MetadataParser.prototype}; | |
17 | |
18 /** | |
19 * Size of the atom header. | |
20 */ | |
21 MpegParser.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 */ | |
28 MpegParser.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 */ | |
54 MpegParser.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 */ | |
62 MpegParser.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 */ | |
151 MpegParser.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 */ | |
165 MpegParser.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 */ | |
207 MpegParser.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 */ | |
241 MpegParser.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 */ | |
263 MpegParser.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 | |
317 MetadataDispatcher.registerParserClass(MpegParser); | |
OLD | NEW |