text: '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';
      }
    });
  }
  