From 8b396423bafbde0c2b7c5b52f4841d7580b49c9d Mon Sep 17 00:00:00 2001 From: "rginda@chromium.org" Date: Fri, 3 Jun 2011 18:36:14 +0000 Subject: MPEG4 metadata parsing for File Browser. Supported tags: brand, duration, fileSize, width, height, author, album, title, thumbnailURL. Thumbnail is currently extracted from the artwork tag (which is probably not the ideal solution). Also refacatored file_manager.js to support pluggable metadata readers. One only needs to add a url regexp and a javascript path to a worker code. Two things done for testing purposes (and will be reverted before committing if needed): 1. added a metadata display in key-value pairs format under the preview image. 2. enabled metadata processing unconditionally (the first line of file_manager.js). BUG=chromium-os:15751 TEST=manual Review URL: http://codereview.chromium.org/6965014 git-svn-id: svn://svn.chromium.org/chrome/trunk/src@87820 0039d316-1c4b-4281-b951-d872f2087c98 --- .../resources/component_extension_resources.grd | 3 + .../resources/file_manager/js/exif_reader.js | 159 ++----------- .../resources/file_manager/js/file_manager.js | 102 +++++---- .../resources/file_manager/js/metadata_reader.js | 111 +++++++++ .../resources/file_manager/js/metadata_util.js | 107 +++++++++ .../resources/file_manager/js/mpeg_reader.js | 251 +++++++++++++++++++++ 6 files changed, 550 insertions(+), 183 deletions(-) create mode 100644 chrome/browser/resources/file_manager/js/metadata_reader.js create mode 100644 chrome/browser/resources/file_manager/js/metadata_util.js create mode 100644 chrome/browser/resources/file_manager/js/mpeg_reader.js diff --git a/chrome/browser/resources/component_extension_resources.grd b/chrome/browser/resources/component_extension_resources.grd index c5701bc..e6c3357 100644 --- a/chrome/browser/resources/component_extension_resources.grd +++ b/chrome/browser/resources/component_extension_resources.grd @@ -26,7 +26,10 @@ + + + diff --git a/chrome/browser/resources/file_manager/js/exif_reader.js b/chrome/browser/resources/file_manager/js/exif_reader.js index c7c43d1..624247b 100644 --- a/chrome/browser/resources/file_manager/js/exif_reader.js +++ b/chrome/browser/resources/file_manager/js/exif_reader.js @@ -3,60 +3,30 @@ // found in the LICENSE file. var exif = { - verbose: false, - - messageHandlers: { - "init": function() { - this.log('thumbnailer initialized'); - }, - - "get-exif": function(fileURL) { - this.processOneFile(fileURL, function callback(metadata) { - postMessage({verb: 'give-exif', - arguments: [fileURL, metadata]}); - }); - }, - }, + urlFilter: /\.jpe?g$/i, - processOneFile: function(fileURL, callback) { - var self = this; + parse : function (file, callback, error_closure, logger) { var currentStep = -1; function nextStep(var_args) { - self.vlog('nextStep: ' + steps[currentStep + 1].name); - steps[++currentStep].apply(self, arguments); + logger.vlog('nextStep: ' + steps[currentStep + 1].name); + steps[++currentStep].apply(null, arguments); } function onError(err) { - self.vlog('Error processing: ' + fileURL + ': step: ' + - steps[currentStep].name + ": " + err); - - postMessage({verb: 'give-exif-error', - arguments: [fileURL, steps[currentStep].name, err]}); + error_closure(err, steps[currentStep].name); } var steps = - [ // Step one, turn the url into an entry. - function getEntry() { - webkitResolveLocalFileSystemURL(fileURL, - function(entry) { nextStep(entry) }, - onError); - }, - - // Step two, turn the entry into a file. - function getFile(entry) { - entry.file(function(file) { nextStep(file) }, onError); - }, - - // Step three, read the file header into a byte array. + [ // Step one, read the file header into a byte array. function readHeader(file) { - var reader = new FileReader(file.webkitSlice(0, 1024)); + var reader = new FileReader(); reader.onerror = onError; reader.onload = function(event) { nextStep(file, reader.result) }; - reader.readAsArrayBuffer(file); + reader.readAsArrayBuffer(file.webkitSlice(0, 1024)); }, - // Step four, find the exif marker and read all exif data. + // Step two, find the exif marker and read all exif data. function findExif(file, buf) { var br = new exif.BufferReader(buf); var mark = br.readMark(); @@ -91,7 +61,7 @@ var exif = { } }, - // Step five, parse the exif data. + // Step three, parse the exif data. function parseExif(file, buf) { var br = new exif.BufferReader(buf); var order = br.readScalar(2); @@ -129,7 +99,7 @@ var exif = { var b64 = br.readBase64(tags[exif.TAG_JPG_THUMB_LENGTH].value); metadata.thumbnailURL = 'data:image/jpeg;base64,' + b64; } else { - self.vlog('Image has EXIF data, but no JPG thumbnail.'); + logger.vlog('Image has EXIF data, but no JPG thumbnail.'); } if (exif.TAG_EXIF_IMAGE_WIDTH in tags) @@ -141,32 +111,11 @@ var exif = { nextStep(metadata); }, - // Step six, we're done. + // Step four, we're done. callback ]; - nextStep(); - }, - - onMessage: function(event) { - var data = event.data; - - if (this.messageHandlers.hasOwnProperty(data.verb)) { - //this.log('dispatching: ' + data.verb + ': ' + data.arguments); - this.messageHandlers[data.verb].apply(this, data.arguments); - } else { - this.log('Unknown message from client: ' + data.verb, data); - } - }, - - log: function(var_args) { - var ary = Array.apply(null, arguments); - postMessage({verb: 'log', arguments: ary}); - }, - - vlog: function(var_args) { - if (this.verbose) - this.log.apply(this, arguments); + nextStep(file); } }; @@ -213,90 +162,26 @@ exif.BufferReader.prototype = { * Big endian read. Most significant bytes come first. */ readBig: function(width) { - var rv = 0; - switch(width) { - case 4: - rv = this.ary_[this.pos_++] << 24; - case 3: - rv |= this.ary_[this.pos_++] << 16; - case 2: - rv |= this.ary_[this.pos_++] << 8; - case 1: - rv |= this.ary_[this.pos_++]; - } - - return rv; + this.skip(width); + return parseUtil.parseBig(this.ary_, this.pos_ - width, width); }, /** * Little endian read. Least significant bytes come first. */ readLittle: function(width) { - var rv = 0; - switch(width) { - case 4: - rv = this.ary_[this.pos_ + 3] << 24; - case 3: - rv |= this.ary_[this.pos_ + 2] << 16; - case 2: - rv |= this.ary_[this.pos_+ 1] << 8; - case 1: - rv |= this.ary_[this.pos_]; - } - - this.pos_ += width; - return rv; + this.skip(width); + return parseUtil.parseLittle(this.ary_, this.pos_ - width, width); }, readString: function(length) { - var chars = []; - for (var i = 0; i < length; i++) { - chars[i] = String.fromCharCode(this.ary_[this.pos_++]); - } - - return chars.join(''); + this.skip(length); + return parseUtil.parseString(this.ary_, this.pos_ - length, this.pos_); }, - base64Alphabet_: ('ABCDEFGHIJKLMNOPQRSTUVWXYZ' + - 'abcdefghijklmnopqrstuvwxyz' + - '0123456789+/').split(''), - readBase64: function(length) { - var rv = []; - var chars = []; - var padding = 0; - - for (var i = 0; i < length; /* incremented inside */) { - var bits = this.ary_[this.pos_ + i++] << 16; - - if (i < length) { - bits |= this.ary_[this.pos_ + i++] << 8; - - if (i < length) { - bits |= this.ary_[this.pos_ + i++]; - } else { - padding = 1; - } - } else { - padding = 2; - } - - chars[3] = this.base64Alphabet_[bits & 63]; - chars[2] = this.base64Alphabet_[(bits >> 6) & 63]; - chars[1] = this.base64Alphabet_[(bits >> 12) & 63]; - chars[0] = this.base64Alphabet_[(bits >> 18) & 63]; - - rv.push.apply(rv, chars); - } - - this.pos_ += i; - - if (padding > 0) - chars[chars.length - 1] = '='; - if (padding > 1) - chars[chars.length - 2] = '='; - - return rv.join(''); + this.skip(length); + return parseUtil.parseBase64(this.ary_, this.pos_ - length, this.pos_); }, readMark: function() { @@ -335,4 +220,4 @@ exif.BufferReader.prototype = { } }; -var onmessage = exif.onMessage.bind(exif); +if (metadataReader) metadataReader.registerParser(exif); diff --git a/chrome/browser/resources/file_manager/js/file_manager.js b/chrome/browser/resources/file_manager/js/file_manager.js index e3b69f6..cb1bb3c 100644 --- a/chrome/browser/resources/file_manager/js/file_manager.js +++ b/chrome/browser/resources/file_manager/js/file_manager.js @@ -4,10 +4,10 @@ // WK Bug 55728 is fixed on the chrome 12 branch but not on the trunk. // TODO(rginda): Enable this everywhere once we have a trunk-worthy fix. -const ENABLE_EXIF_READER = navigator.userAgent.match(/chrome\/12\.0/i); +const ENABLE_METADATA = true; // Thumbnail view is painful without the exif reader. -const ENABLE_THUMBNAIL_VIEW = ENABLE_EXIF_READER; +const ENABLE_THUMBNAIL_VIEW = ENABLE_METADATA; var g_slideshow_data = null; @@ -41,7 +41,7 @@ function FileManager(dialogDom, rootEntries, params) { this.listType_ = null; - this.exifCache_ = {}; + this.metadataCache_ = {}; // True if we should filter out files that start with a dot. this.filterFiles_ = true; @@ -79,10 +79,16 @@ function FileManager(dialogDom, rootEntries, params) { this.table_.list_.focus(); - if (ENABLE_EXIF_READER) { - this.exifReader = new Worker('js/exif_reader.js'); - this.exifReader.onmessage = this.onExifReaderMessage_.bind(this); - this.exifReader.postMessage({verb: 'init'}); + if (ENABLE_METADATA) { + // Pass all URLs to the metadata reader until we have a correct filter. + this.metadataUrlFilter_ = /.*/; + + this.metadataReader_ = new Worker('js/metadata_reader.js'); + this.metadataReader_.onmessage = this.onMetadataMessage_.bind(this); + this.metadataReader_.postMessage({verb: 'init'}); + } else { + // This regexp never matches anything, so the metadata reader is bypassed. + this.metadataUrlFilter_ = /$./; } } @@ -1132,10 +1138,10 @@ FileManager.prototype = { cacheNextFile(); }; - FileManager.prototype.onExifGiven_ = function(fileURL, metadata) { - var observers = this.exifCache_[fileURL]; + FileManager.prototype.onMetadataReceived_ = function(fileURL, metadata) { + var observers = this.metadataCache_[fileURL]; if (!observers || !(observers instanceof Array)) { - console.error('Missing or invalid exif observers: ' + fileURL + ': ' + + console.error('Missing or invalid metadata observers: ' + fileURL + ': ' + observers); return; } @@ -1144,15 +1150,19 @@ FileManager.prototype = { observers[i](metadata); } - this.exifCache_[fileURL] = metadata; + this.metadataCache_[fileURL] = metadata; + }; + + FileManager.prototype.onMetadataError_ = function(fileURL, step, error) { + console.warn('metadata: ' + fileURL + ': ' + step + ': ' + error); + this.onMetadataReceived_(fileURL, {}); }; - FileManager.prototype.onExifError_ = function(fileURL, step, error) { - console.warn('Exif error: ' + fileURL + ': ' + step + ': ' + error); - this.onExifGiven_(fileURL, {}); + FileManager.prototype.onMetadataUrlFilter_ = function(regexp) { + this.metadataUrlFilter_ = regexp; }; - FileManager.prototype.onExifReaderMessage_ = function(event) { + FileManager.prototype.onMetadataMessage_ = function(event) { var data = event.data; var self = this; @@ -1160,19 +1170,23 @@ FileManager.prototype = { switch (data.verb) { case 'log': - console.log.apply(console, ['exif:'].concat(data.arguments)); + console.log.apply(console, ['metadata:'].concat(data.arguments)); + break; + + case 'result': + fwd('onMetadataReceived_', data.arguments); break; - case 'give-exif': - fwd('onExifGiven_', data.arguments); + case 'error': + fwd('onMetadataError_', data.arguments); break; - case 'give-exif-error': - fwd('onExifError_', data.arguments); + case 'filter': + fwd('onMetadataUrlFilter_', data.arguments); break; default: - console.log('Unknown message from exif reader: ' + data.verb, data); + console.log('Unknown message from metadata reader: ' + data.verb, data); break; } }; @@ -1335,15 +1349,20 @@ FileManager.prototype = { }); }; - FileManager.prototype.cacheExifMetadata_ = function(entry, callback) { + FileManager.prototype.cacheMetadata_ = function(entry, callback) { var url = entry.toURL(); - var cacheValue = this.exifCache_[url]; + var cacheValue = this.metadataCache_[url]; if (!cacheValue) { // This is the first time anyone's asked, go get it. - this.exifCache_[url] = [callback]; - this.exifReader.postMessage({verb: 'get-exif', - arguments: [entry.toURL()]}); + if (entry.name.match(this.metadataUrlFilter_)) { + this.metadataCache_[url] = [callback]; + this.metadataReader_.postMessage( + {verb: 'request', arguments: [entry.toURL()]}); + return; + } + // Cannot extract metadata for this file, return an empty map. + setTimeout(function() { callback({}) }); return; } @@ -1359,7 +1378,7 @@ FileManager.prototype = { return; } - console.error('Unexpected exif cache value:' + cacheValue); + console.error('Unexpected metadata cache value:' + cacheValue); }; FileManager.prototype.getThumbnailURL = function(entry, callback) { @@ -1367,28 +1386,19 @@ FileManager.prototype = { return; var iconType = getIconType(entry); - if (iconType != 'image') { - // Not an image, display a canned clip-art graphic. - if (!(iconType in previewArt)) - iconType = 'unknown'; - - setTimeout(function() { callback(iconType, previewArt[iconType]) }); - return; - } - if (ENABLE_EXIF_READER) { - if (entry.name.match(/\.jpe?g$/i)) { - // File is a jpg image, fetch the exif thumbnail. - this.cacheExifMetadata_(entry, function(metadata) { - callback(iconType, metadata.thumbnailURL || entry.toURL()); - }); - return; + this.cacheMetadata_(entry, function (metadata) { + var url = metadata.thumbnailURL; + if (!url) { + if (iconType == 'image') { + url = entry.toURL(); + } else { + url = previewArt[iconType] || previewArt['unknown']; + } } - } - // File is some other kind of image, just return the url to the whole - // thing. - setTimeout(function() { callback(iconType, entry.toURL()) }); + callback(iconType, url); + }); }; /** diff --git a/chrome/browser/resources/file_manager/js/metadata_reader.js b/chrome/browser/resources/file_manager/js/metadata_reader.js new file mode 100644 index 0000000..aa7b133 --- /dev/null +++ b/chrome/browser/resources/file_manager/js/metadata_reader.js @@ -0,0 +1,111 @@ +// Copyright (c) 2011 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. + +var metadataReader = { + verbose: false, + + parsers: [], + + registerParser : function(parser) { + this.parsers.push(parser); + }, + + messageHandlers: { + "init": function() { + // Combine url filters from all parsers and build a single regexp. + var sources = []; + for (var i = 0; i != this.parsers.length; i++) { + sources.push(this.parsers[i].urlFilter.source); + } + var regexp = new RegExp('(' + sources.join('|') + ')', 'i'); + postMessage({verb: 'filter', arguments: [regexp]}); + this.log('initialized with URL filter ' + regexp); + }, + + "request": function(fileURL) { + this.processOneFile(fileURL, function callback(metadata) { + postMessage({verb: 'result', arguments: [fileURL, metadata]}); + }); + } + }, + + onMessage: function(event) { + var data = event.data; + + if (this.messageHandlers.hasOwnProperty(data.verb)) { + this.messageHandlers[data.verb].apply(this, data.arguments); + } else { + this.log('Unknown message from client: ' + data.verb, data); + } + }, + + error: function(var_args) { + var ary = Array.apply(null, arguments); + postMessage({verb: 'error', arguments: ary}); + }, + + log: function(var_args) { + var ary = Array.apply(null, arguments); + postMessage({verb: 'log', arguments: ary}); + }, + + vlog: function(var_args) { + if (this.verbose) + this.log.apply(this, arguments); + }, + + processOneFile: function(fileURL, callback) { + var self = this; + var currentStep = -1; + + function nextStep(var_args) { + self.vlog('nextStep: ' + steps[currentStep + 1].name); + steps[++currentStep].apply(self, arguments); + } + + function onError(err, stepName) { + self.error(fileURL, stepName || steps[currentStep].name, err); + } + + var steps = + [ // Step one, find the parser matching the url. + function detectFormat() { + for (var i = 0; i != self.parsers.length; i++) { + var parser = self.parsers[i]; + if (fileURL.match(parser.urlFilter)) { + nextStep(parser.parse); + return; + } + } + onError('unsupported format'); + }, + + // Step two, turn the url into an entry. + function getEntry(parseFunc) { + webkitResolveLocalFileSystemURL( + fileURL, + function(entry) { nextStep(entry, parseFunc) }, + onError); + }, + + // Step three, turn the entry into a file. + function getFile(entry, parseFunc) { + entry.file(function(file) { nextStep(file, parseFunc) }, onError); + }, + + // Step four, parse the file content. + function parseContent(file, parseFunc) { + parseFunc(file, callback, onError, self); + } + ]; + + nextStep(); + } +}; + +var onmessage = metadataReader.onMessage.bind(metadataReader); + +importScripts("metadata_util.js"); +importScripts("exif_reader.js"); +importScripts("mpeg_reader.js"); \ No newline at end of file diff --git a/chrome/browser/resources/file_manager/js/metadata_util.js b/chrome/browser/resources/file_manager/js/metadata_util.js new file mode 100644 index 0000000..89231b1 --- /dev/null +++ b/chrome/browser/resources/file_manager/js/metadata_util.js @@ -0,0 +1,107 @@ +// Copyright (c) 2011 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. + + +var parseUtil = { + /** + * Big endian read. Most significant bytes come first. + */ + parseBig: function(bytes, pos, width) { + var rv = 0; + switch(width) { + case 4: + rv = bytes[pos++] << 24; + case 3: + rv |= bytes[pos++] << 16; + case 2: + rv |= bytes[pos++] << 8; + case 1: + rv |= bytes[pos++]; + } + return rv; + }, + + /** + * Little endian read. Least significant bytes come first. + */ + parseLittle: function(bytes, pos, width) { + var rv = 0; + switch(width) { + case 4: + rv = bytes[pos + 3] << 24; + case 3: + rv |= bytes[pos + 2] << 16; + case 2: + rv |= bytes[pos + 1] << 8; + case 1: + rv |= bytes[pos]; + } + return rv; + }, + + /** + * Read a string. + * TODO: implement UTF8. + */ + parseString: function(bytes, pos, end) { + var codes = []; + while (pos < end) { + codes.push(bytes[pos++]); + } + return String.fromCharCode.apply(null, codes); + }, + + base64Alphabet: ('ABCDEFGHIJKLMNOPQRSTUVWXYZ' + + 'abcdefghijklmnopqrstuvwxyz' + + '0123456789+/').split(''), + + parseBase64: function(bytes, pos, end) { + var rv = []; + var chars = []; + var padding = 0; + + while (pos < end) { + var bits = bytes[pos++] << 16; + + if (pos < end) { + bits |= bytes[pos++] << 8; + + if (pos < end) { + bits |= bytes[pos++]; + } else { + padding = 1; + } + } else { + padding = 2; + } + + chars[3] = parseUtil.base64Alphabet[bits & 63]; + chars[2] = parseUtil.base64Alphabet[(bits >> 6) & 63]; + chars[1] = parseUtil.base64Alphabet[(bits >> 12) & 63]; + chars[0] = parseUtil.base64Alphabet[(bits >> 18) & 63]; + + rv.push.apply(rv, chars); + } + + if (padding > 0) + chars[chars.length - 1] = '='; + if (padding > 1) + chars[chars.length - 2] = '='; + + return rv.join(''); + }, + + createImageDataUrl: function(bytes, pos, end) { + var format; + if (parseUtil.parseString(bytes, pos, 4) == '\x89PNG') { + format = 'png'; + } else if (parseUtil.parseBig(bytes, pos, 2) == 0xFFD8) { + format = 'jpeg'; + } else { + format = 'unknown'; + } + var b64 = parseUtil.parseBase64(bytes, pos, end); + return 'data:image/' + format + ';base64,' + b64; + } +} \ No newline at end of file diff --git a/chrome/browser/resources/file_manager/js/mpeg_reader.js b/chrome/browser/resources/file_manager/js/mpeg_reader.js new file mode 100644 index 0000000..ed2aec1 --- /dev/null +++ b/chrome/browser/resources/file_manager/js/mpeg_reader.js @@ -0,0 +1,251 @@ +// Copyright (c) 2011 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. + +var mpeg = { + urlFilter: /\.(mp4|m4v|m4a|mpe?g4?)$/i, + + parse: function (file, callback, onError, logger) { + const HEADER_SIZE = 8; + + var metadata = {}; + + // MPEG-4 format parsing primitives. + + function checkBuffer(bytes, pos, end) { + if (!end) end = bytes.length; + if (pos > end) + throw "Reading past the buffer end: pos=" + pos + ", end=" + end; + } + + function parseFourCC(bytes, pos, end) { + checkBuffer(bytes, pos + 4, end); + return parseUtil.parseString(bytes, pos, pos + 4); + } + + function parseUint16(bytes, pos, end) { + checkBuffer(bytes, pos + 2, end) + return parseUtil.parseBig(bytes, pos, 2); + } + + function parseUint32(bytes, pos, end) { + checkBuffer(bytes, pos + 4, end) + return parseUtil.parseBig(bytes, pos, 4); + } + + function parseAtomSize(bytes, pos, end) { + var size = parseUint32(bytes, pos, end); + + if (size < HEADER_SIZE) + throw "atom too short (" + size + ") @" + pos; + + if (end && (end < pos + size)) + throw "atom too long (" + size + ") @" + pos; + + return size; + } + + function parseAtomName(bytes, pos, end) { + return parseFourCC(bytes, pos, end).toLowerCase(); + } + + function findParentAtom(atom, name) { + for (;;) { + atom = atom.parent; + if (!atom) return null; + if (atom.name == name) return atom; + } + } + + // Specific atom parsers. + function parseFtyp(bytes, atom) { + metadata.brand = parseFourCC(bytes, atom.start, atom.end); + } + + function parseMvhd(bytes, atom) { + var version = parseUint32(bytes, atom.start, atom.end); + var offset = (version == 0) ? 12 : 20; + var timescale = parseUint32(bytes, atom.start + offset, atom.end) + var duration = parseUint32(bytes, atom.start + offset + 4, atom.end) + metadata.duration = duration / timescale; + } + + function parseHdlr(bytes, atom) { + findParentAtom(atom, 'trak').trackType = + parseFourCC(bytes, atom.start + 8, atom.end); + } + + function parseStsd(bytes, atom) { + var track = findParentAtom(atom, 'trak'); + if (track && track.trackType == 'vide') { + metadata.width = parseUint16(bytes, atom.start + 40, atom.end); + metadata.height = parseUint16(bytes, atom.start + 42, atom.end); + } + } + + function parseDataString(name, bytes, atom) { + metadata[name] = parseUtil.parseString(bytes, atom.start + 8, atom.end); + } + + function parseCovr(bytes, atom) { + metadata.thumbnailURL = + parseUtil.createImageDataUrl(bytes, atom.start + 8, 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. + var parseRoot = { + ftyp: parseFtyp, + moov: { + mvhd : parseMvhd, + trak: { + mdia: { + hdlr: parseHdlr, + minf: { + stbl: { + stsd: parseStsd + } + } + }, + meta: parseMeta + }, + udta: { + meta: parseMeta + }, + meta: parseMeta + }, + meta: parseMeta + }; + + function applyParser(parser, bytes, atom, file_pos) { + if (logger.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 - HEADER_SIZE; + logger.vlog(path, + '@' + (file_pos + start) + ':' + (atom.end - start), + action); + } + + if (parser) { + if (parser instanceof Function) { + parser(bytes, atom); + } else { + if (parser.versioned) { + atom.start += 4; + } + parseMpegAtomsInRange(parser, bytes, atom, file_pos); + } + } + } + + function parseMpegAtomsInRange(parser, bytes, parentAtom, file_pos) { + 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; + + var size = parseAtomSize(bytes, offset, parentAtom.end); + var name = parseAtomName(bytes, offset + 4, parentAtom.end); + + applyParser( + parser[name], + bytes, + { start: offset + HEADER_SIZE, + end: offset + size, + name: name, + parent: parentAtom + }, + file_pos + ); + + offset += size; + } + } + + function requestRead(file, file_pos, size, name) { + var reader = new FileReader(); + reader.onerror = onError; + reader.onload = function(event) { + processTopLevelAtom(file, reader.result, file_pos, size, name); + }; + logger.vlog("reading @" + file_pos + ":" + size); + reader.readAsArrayBuffer(file.webkitSlice(file_pos, file_pos + size)); + } + + function processTopLevelAtom(file, buf, file_pos, size, name) { + try { + var bytes = new Uint8Array(buf); + + var atom_end = size - HEADER_SIZE; // the header has already been read. + + // Check the available data size. It should be either exactly + // what we requested or HEADER_SIZE bytes less (for the last atom). + if (bytes.length != atom_end && bytes.length != size) { + throw "Read failure @" + file_pos + ", " + + "requested " + size + ", read " + bytes.length; + } + + // Process the top level atom. + if (name) { // name is undefined only the first time. + applyParser( + parseRoot[name], + bytes, + {start: 0, end: atom_end, name: name}, + file_pos + ); + } + + file_pos += bytes.length; + if (bytes.length == 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. + var next_size = parseAtomSize(bytes, bytes.length - HEADER_SIZE); + var next_name = parseAtomName(bytes, bytes.length - HEADER_SIZE + 4); + + // 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 (!parseRoot[next_name]) { + file_pos += next_size - HEADER_SIZE; + next_size = HEADER_SIZE; + } + + requestRead(file, file_pos, next_size, next_name); + } else { + // The previous read did not return the next atom header, EOF reached. + logger.vlog("EOF @" + file_pos); + metadata.fileSize = file_pos; + callback(metadata); + } + } catch(e) { + return onError(e); + } + } + + // Kick off the processing by reading the first atom's header. + requestRead(file, 0, HEADER_SIZE); + } +}; + +if (metadataReader) metadataReader.registerParser(mpeg); \ No newline at end of file -- cgit v1.1