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