Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
path: root/shared/js/media/get_video_rotation.js
diff options
context:
space:
mode:
Diffstat (limited to 'shared/js/media/get_video_rotation.js')
-rw-r--r--shared/js/media/get_video_rotation.js143
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';
+ }
+ });
+}