diff options
Diffstat (limited to 'shared/js/media/get_video_rotation.js')
-rw-r--r-- | shared/js/media/get_video_rotation.js | 143 |
1 files changed, 143 insertions, 0 deletions
diff --git a/shared/js/media/get_video_rotation.js b/shared/js/media/get_video_rotation.js new file mode 100644 index 0000000..3359b92 --- /dev/null +++ b/shared/js/media/get_video_rotation.js @@ -0,0 +1,143 @@ +'use strict'; + +// +// Given an MP4/Quicktime based video file as a blob, read through its +// atoms to find the track header "tkhd" atom and extract the rotation +// matrix from it. Convert the matrix value to rotation in degrees and +// pass that number to the specified callback function. If no value is +// found but the video file is valid, pass null to the callback. If +// any errors occur, pass an error message (a string) callback. +// +// See also: +// http://androidxref.com/4.0.4/xref/frameworks/base/media/libstagefright/MPEG4Writer.cpp +// https://developer.apple.com/library/mac/#documentation/QuickTime/QTFF/QTFFChap2/qtff2.html +// +function getVideoRotation(blob, rotationCallback) { + + // A utility for traversing the tree of atoms in an MP4 file + function MP4Parser(blob, handlers) { + // Start off with a 1024 chunk from the start of the blob. + BlobView.get(blob, 0, 1024, function(data, error) { + // Make sure that the blob is, in fact, some kind of MP4 file + if (data.getASCIIText(4, 4) !== 'ftyp') { + handlers.errorHandler('not an MP4 file'); + return; + } + parseAtom(data); + }); + + // Call this with a BlobView object that includes the first 16 bytes of + // an atom. It doesn't matter whether the body of the atom is included. + function parseAtom(data) { + var offset = data.sliceOffset + data.viewOffset; // atom position in blob + var size = data.readUnsignedInt(); // atom length + var type = data.readASCIIText(4); // atom type + var contentOffset = 8; // position of content + + if (size === 0) { + // Zero size means the rest of the file + size = blob.size - offset; + } + else if (size === 1) { + // A size of 1 means the size is in bytes 8-15 + size = data.readUnsignedInt() * 4294967296 + data.readUnsignedInt(); + contentOffset = 16; + } + + var handler = handlers[type] || handlers.defaultHandler; + if (typeof handler === 'function') { + // If the handler is a function, pass that function a + // DataView object that contains the entire atom + // including size and type. Then use the return value + // of the function as instructions on what to do next. + data.getMore(data.sliceOffset + data.viewOffset, size, function(atom) { + // Pass the entire atom to the handler function + var rv = handler(atom); + + // If the return value is 'done', stop parsing. + // Otherwise, continue with the next atom. + // XXX: For more general parsing we need a way to pop some + // stack levels. A return value that is an atom name should mean + // pop back up to this atom type and go on to the next atom + // after that. + if (rv !== 'done') { + parseAtomAt(data, offset + size); + } + }); + } + else if (handler === 'children') { + // If the handler is this string, then assume that the atom is + // a container atom and do its next child atom next + var skip = (type === 'meta') ? 4 : 0; // special case for meta atoms + parseAtomAt(data, offset + contentOffset + skip); + } + else if (handler === 'skip' || !handler) { + // Skip the atom entirely and go on to the next one. + // If there is no next one, call the eofHandler or just return + parseAtomAt(data, offset + size); + } + else if (handler === 'done') { + // Stop parsing + return; + } + } + + function parseAtomAt(data, offset) { + if (offset >= blob.size) { + if (handlers.eofHandler) + handlers.eofHandler(); + return; + } + else { + data.getMore(offset, 8, parseAtom); + } + } + } + + // We want to loop through the top-level atoms until we find the 'moov' + // atom. Then, within this atom, there are one or more 'trak' atoms. + // Each 'trak' should begin with a 'tkhd' atom. The tkhd atom has + // a transformation matrix at byte 48. The matrix is 9 32 bit integers. + // We'll interpret those numbers as a rotation of 0, 90, 180 or 270. + // If the video has more than one track, we expect all of them to have + // the same rotation, so we'll only look at the first 'trak' atom that + // we find. + MP4Parser(blob, { + errorHandler: function(msg) { rotationCallback(msg); }, + eofHandler: function() { rotationCallback(null); }, + defaultHandler: 'skip', // Skip all atoms other than those listed below + moov: 'children', // Enumerate children of the moov atom + trak: 'children', // Enumerate children of the trak atom + tkhd: function(data) { // Pass the tkhd atom to this function + // The matrix begins at byte 48 + data.advance(48); + + var a = data.readUnsignedInt(); + var b = data.readUnsignedInt(); + data.advance(4); // we don't care about this number + var c = data.readUnsignedInt(); + var d = data.readUnsignedInt(); + + if (a === 0 && d === 0) { // 90 or 270 degrees + if (b === 0x00010000 && c === 0xFFFF0000) + rotationCallback(90); + else if (b === 0xFFFF0000 && c === 0x00010000) + rotationCallback(270); + else + rotationCallback('unexpected rotation matrix'); + } + else if (b === 0 && c === 0) { // 0 or 180 degrees + if (a === 0x00010000 && d === 0x00010000) + rotationCallback(0); + else if (a === 0xFFFF0000 && d === 0xFFFF0000) + rotationCallback(180); + else + rotationCallback('unexpected rotation matrix'); + } + else { + rotationCallback('unexpected rotation matrix'); + } + return 'done'; + } + }); +} |