text: /* global playerShowing,showThrobber,hideThrobber,updateDialog,videodb,
    addVideo,getVideoRotation,THUMBNAIL_WIDTH,THUMBNAIL_HEIGHT,MediaDB */
  /* exported addToMetadataQueue,stopParsingMetadata */
  // This file is part of the Gaia Video app.
  //
  // It includes functions to obtain metadata for video files. The most
  // important parts of this metadata are obtained by loading the video into
  // an offscreen <video> element.
  //
  // The phones we want to run on have only a single hardware h.264 video
  // decoder, and gecko is not yet smart enough to share the hardware among
  // all the <video> elements that want to use it. So all apps (camera,
  // gallery and video) that have permission to use the video hardware must be
  // careful to only use it while they are in the foreground and to relinquish
  // it when they go to the background.
  //
  // The video app wants to use <video> elements for metadata parsing and for
  // playing videos for the user. It can't do both of these things at the same
  // time. The MediaDB metadata parsing architecture assumes that it can,
  // however. So the Video app does not pass a metadata parser function to
  // MediaDB. MediaDB notifies the app about new video files but does not
  // include any metadata with those notifications. Instead, we use the
  // queueing and metadata parsing functions in this file to handle metadata
  // parsing in an interruptible way.
  //
  // When we have a new video file that needs metadata, we pass it to
  // addToMetadataQueue(). When we need to stop parsing metadata (because we
  // want to play a video or because we went to the background) we call
  // stopParsingMetadata(). And when we're able to parse metadata again we
  // call startParsingMetadata(). When a queued video is processed and its
  // metadata is ready, it is saved to MediaDB, and then addVideo() is invoked
  // to display the video title and thumbnail.
  //
  'use strict';
  
  var metadataQueue = [];
  var processingQueue = false;
  var stopParsingMetadataCallback = null;
  // noMoreWorkCallback is fired when the queue is processed or empty.
  var noMoreWorkCallback = null;
  
  // This function queues a fileinfo object with no metadata. When the app is
  // able it will obtain metadata for the video and pass the updated fileinfo
  // to addVideo() so that it becomes visible to the user.
  function addToMetadataQueue(fileinfo) {
    metadataQueue.push(fileinfo);
    startParsingMetadata();
  }
  
  // Start or resume metadata parsing, if conditions are right
  function startParsingMetadata() {
    // If we're already working, return right away
    if (processingQueue) {
      return;
    }
  
    // If there is no work queued, fire noMoreWorkCallback event and return right
    // away.
    if (metadataQueue.length === 0) {
      if (noMoreWorkCallback) {
        noMoreWorkCallback();
        noMoreWorkCallback = null;
      }
      return;
    }
  
    // Don't parse metadata if we are not the foreground app. When we're
    // in the background we need to allow the foreground app to use the
    // video hardware. Also, if the video player is showing then we
    // can't parse metadata because we're already using the video hardware.
    if (document.hidden || playerShowing) {
      return;
    }
  
    // If our MediaDB object is not ready, it means that we can't read
    // video files so can't process metadata yet. We just return in this
    // case. The onready event handler in db.js will call this function
    // when the db becomes ready.
    if (videodb.state !== MediaDB.READY) {
      return;
    }
  
    // Start processing the queue
    processingQueue = true;
    showThrobber();
    processFirstQueuedItem();
  }
  
  // Stop parsing metadata. If a callback is specified, call it when stopped.
  function stopParsingMetadata(callback) {
    // If we're not processing metadata, just call the callback right away
    if (!processingQueue) {
      if (callback) {
        callback();
      }
      return;
    }
  
    // Otherwise, request a stop.
    // This variable acts as a flag to prevent processFirstQueuedItem from
    // creating any more metadatas.
    stopParsingMetadataCallback = callback || true;
  }
  
  // If we've been asked to stop processing, then run the callback, and stop.
  // Otherwise, if there is nothing in the queue, do nothing.  Otherwise, take
  // the first item off the queue, obtain metadata for it, save the metadata
  // to the database, and then call addVideo() to display the newly processed
  // video to the user. Finally, use setTimeout() to schedule a new call to
  // this same function.
  //
  // XXX: are there any race conditions here?
  // Are we guaranteed to always stop and to always call the callback?
  //
  function processFirstQueuedItem() {
    // If the stop flag is set, call the callback and stop
    if (stopParsingMetadataCallback) {
      var callback = stopParsingMetadataCallback;
      stopParsingMetadataCallback = null;
      processingQueue = false;
      hideThrobber();
  
      if (callback !== true) {
        callback();  // Okay, we've stopped.
      }
      return;
    }
  
    // If there is no work queued, update dialog in case there are no playable
    // videos; then return right away
    if (metadataQueue.length === 0) {
      processingQueue = false;
      hideThrobber();
      updateDialog();
  
      // Check explicitly for noMoreWorkCallBack as function, see bug 972651
      if (typeof(noMoreWorkCallback) === 'function') {
        noMoreWorkCallback();
      }
      return;
    }
  
    // Otherwise, take a fileinfo object from the front of the queue and
    // process it with a <video> tag to obtain size, duration and a poster
    // image. This happens asynchronously. When it is done, we call
    // processFirstQueuedItem() again to re-check the stop flag and process
    // the next item on the queue.
    var fileinfo = metadataQueue.shift();
  
    // XXX
    // The code below is brittle because it does not have exception handlers.
    // If anything goes wrong with this getFile() call or any of the nexted
    // functions that get called asynchronously, then we may never advance to
    // the next item in the queue and the metadata parsing process will just
    // freeze up forever. The fix, I suppose is to add try/catch for each
    // async step (or to convert to promises). See Bug 1229651 for a specific
    // example of what happens when getFile() throws an exception.
    videodb.getFile(fileinfo.name, function(file) {
      getMetadata(file, function(metadata) {
        // Associate the metadata with this fileinfo object
        fileinfo.metadata = metadata;
  
        // Save it to the database
        videodb.updateMetadata(fileinfo.name, metadata, function() {
          // Create and insert a thumbnail for the video
          if (metadata.isVideo) {
            videodb.getFileInfo(fileinfo.name, function(dbfileinfo) {
              addVideo(dbfileinfo);
            });
          }
        });
  
        // And process the next video in the queue
        setTimeout(processFirstQueuedItem);
      });
    }, function(err) {
      console.error('getFile error: ', fileinfo.name, err);
      processFirstQueuedItem();
    });
  }
  
  // Given a video File object, asynchronously pass an object of metadata to
  // the specified callback.
  function getMetadata(videofile, callback) {
    // This is the video element that will get the metadata for us.
    // Because of an apparent bug in gecko, it needs to be here rather than
    // something that is shared globally.
    var offscreenVideo = document.createElement('video');
    var metadata = {};
  
    // If its a video type we don't know how to play, ignore it.
    if (!offscreenVideo.canPlayType(videofile.type)) {
      metadata.isVideo = false;
      callback(metadata);
      return;
    }
  
    // Create a blob: url for the file. It will be revoked in unload().
    var url = URL.createObjectURL(videofile);
  
    // Load the video into an offscreen <video> element.
    offscreenVideo.preload = 'metadata';
    offscreenVideo.src = url;
  
    //
    // There seems to have been a gecko regression, and error events are no
    // longer being fired when we try to play some zero-length or truncated
    // video files. To workaround this, we use a timeout to assume the
    // video is unplayable if we nave not gotten any events within a reasonable
    // amount of time.
    //
    // See bug 1198169
    //
    const METADATA_TIMEOUT = 3000;
    var timeoutId = setTimeout(function() {
      console.error('No loadedmetadata or error events seen yet.',
                    'Assuming video file is corrupt:', videofile.name);
      offscreenVideo.onerror();
    }, METADATA_TIMEOUT);
  
    offscreenVideo.onerror = function(e) {
      // Something went wrong. Maybe the file was corrupt?
      console.error('Can\'t play video', videofile.name, e);
      metadata.isVideo = false;
      clearTimeout(timeoutId);
      unload();
      callback(metadata);
    };
  
    offscreenVideo.onloadedmetadata = function() {
      // We got metadata, so we don't need the timeout
      clearTimeout(timeoutId);
  
      // If videoWidth is 0 then this is likely an audio file (ogg / mp4)
      // with an ambiguous filename extension that makes it look like a video.
      // This test only works correctly if we're using a new offscreen video
      // element each time. If I try to make the element a global, then it
      // retains the size of the last video when a non-video is loaded.
      if (!offscreenVideo.videoWidth) {
        metadata.isVideo = false;
        unload();
        callback(metadata);
        return;
      }
  
      // Otherwise it is a video!
      metadata.isVideo = true;
  
      // read title from metadata or fallback to filename.
      metadata.title = readFromMetadata('title') ||
                       fileNameToVideoName(videofile.name);
  
      // The video element tells us the video duration and size.
      metadata.duration = offscreenVideo.duration;
      metadata.width = offscreenVideo.videoWidth;
      metadata.height = offscreenVideo.videoHeight;
  
      // If this is a .3gp video file, look for its rotation matrix and
      // then create the thumbnail. Otherwise set rotation to 0 and
      // create the thumbnail immediately.  getVideoRotation is defined
      // in shared/js/media/get_video_rotation.js
      if (/.3gp$/.test(videofile.name)) {
        getVideoRotation(videofile, function(rotation) {
          if (typeof rotation === 'number') {
            metadata.rotation = rotation;
          }
          else if (typeof rotation === 'string') {
            console.warn('Video rotation:', rotation);
          }
          createThumbnail();
        });
      } else {
        metadata.rotation = 0;
        createThumbnail();
      }
    };
  
    // The text case of key in metadata is not always lower or upper cases. That
    // depends on the creation tools. This function helps to read keys in lower
    // cases and returns the value of corresponding key.
    function readFromMetadata(lowerCaseKey) {
      var tags = offscreenVideo.mozGetMetadata();
      for (var key in tags) {
        // to lower case and match it.
        if (key.toLowerCase() === lowerCaseKey) {
          return tags[key];
        }
      }
      // no matched key, return undefined.
      return;
    }
  
    function createThumbnail() {
      // Videos often begin with a black screen, so skip ahead 5 seconds
      // or 1/10th of the video, whichever is shorter in the hope that we'll
      // get a more interesting thumbnail that way.
      // Because of bug 874692, corrupt video files may not be seekable,
      // and may not fire an error event, so if we aren't able to seek
      // after a certain amount of time, we'll abort and assume that the
      // video is invalid.
      var t = Math.min(5, offscreenVideo.duration / 10);
      offscreenVideo.fastSeek(t);
  
      var failed = false;                      // Did seeking fail?
      var timeout = setTimeout(fail, 10000);   // Fail after 10 seconds
      offscreenVideo.onerror = fail;           // Or if we get an error event
      function fail() {                        // Seeking failure case
        console.warn('Seek failed while creating thumbnail for', videofile.name,
                     '. Ignoring corrupt file.');
        failed = true;
        clearTimeout(timeout);
        offscreenVideo.onerror = null;
        metadata.isVideo = false;
        unload();
        callback(metadata);
      }
      offscreenVideo.onseeked = function() {   // Seeking success case
        // Avoid race condition: if we already failed, stop now
        if (failed) {
          return;
        }
        clearTimeout(timeout);
        captureFrame(offscreenVideo, metadata, function(poster) {
          metadata.poster = poster;
          unload();
          callback(metadata); // We've got all the metadata we need now.
        });
      };
    }
  
    // Free the resources being used by the offscreen video element
    function unload() {
      URL.revokeObjectURL(offscreenVideo.src);
      offscreenVideo.removeAttribute('src');
      offscreenVideo.load();
    }
  
    // Derive the video title from its filename.
    function fileNameToVideoName(filename) {
      filename = filename.split('/').pop()
        .replace(/\.(webm|ogv|ogg|mp4|3gp)$/, '')
        .replace(/[_\.]/g, ' ');
      return filename.charAt(0).toUpperCase() + filename.slice(1);
    }
  }
  
  function captureFrame(player, metadata, callback) {
    try {
      // Create a new canvas, and set its size
      var canvas = document.createElement('canvas');
      canvas.width = THUMBNAIL_WIDTH;
      canvas.height = THUMBNAIL_HEIGHT;
  
      // Now create the context for the canvas after the size is set.
      // The flag is a hint that we're going to be calling toBlob() on
      // this canvas and that it is more efficient to use a software canvas
      // than it is to do this on the GPU.
      var ctx = canvas.getContext('2d', { willReadFrequently: true });
  
      var vw = player.videoWidth, vh = player.videoHeight;
      var tw, th;
  
      // If a rotation is specified, rotate the canvas context
      switch (metadata.rotation) {
      case 90:
        ctx.translate(THUMBNAIL_WIDTH, 0);
        ctx.rotate(Math.PI / 2);
        tw = THUMBNAIL_HEIGHT;
        th = THUMBNAIL_WIDTH;
        break;
      case 180:
        ctx.translate(THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT);
        ctx.rotate(Math.PI);
        tw = THUMBNAIL_WIDTH;
        th = THUMBNAIL_HEIGHT;
        break;
      case 270:
        ctx.translate(0, THUMBNAIL_HEIGHT);
        ctx.rotate(-Math.PI / 2);
        tw = THUMBNAIL_HEIGHT;
        th = THUMBNAIL_WIDTH;
        break;
      default:
        tw = THUMBNAIL_WIDTH;
        th = THUMBNAIL_HEIGHT;
        break;
      }
  
      // Need to find the minimum ratio between heights and widths,
      // so the image (especailly the square thumbnails) would fit
      // in the container with right ratio and no extra stretch.
      // x and y are right/left and top/bottom margins and where we
      // start drawing the image. Since we scale the image, x and y
      // will be scaled too. Below gives us x and y actual pixels
      // without scaling.
      var scale = Math.min(tw / vw, th / vh),
          w = scale * vw, h = scale * vh,
          x = (tw - w) / 2 / scale, y = (th - h) / 2 / scale;
  
      // Scale the image
      ctx.scale(scale, scale);
  
      // Draw the current video frame into the image
      ctx.drawImage(player, x, y);
  
      // Convert it to an image file and pass to the callback.
      canvas.toBlob(done, 'image/jpeg');
    }
    catch (e) {
      console.error('Exception in captureFrame:', e, e.stack);
      done(null);
    }
  
    function done(blob) {
      canvas.width = 0;    // Free canvas memory
      ctx = canvas = null; // Prevent leaks, just to be safe
      callback(blob);      // Return the frame thumbnail to the caller
    }
  }
  