Permalink
Browse files

Merge pull request #8148 from davidflanagan/bug837236-v1.0.1

Bug 837236 - share video decoding hardware among Camera, Gallery and Video r=daleharvey a=tef+
  • Loading branch information...
2 parents 5e064fb + 53959d7 commit 4164424049f9caa4e6b26aefe3a4fa7421451b2b @davidflanagan davidflanagan committed Feb 17, 2013
View
193 apps/camera/js/filmstrip.js
@@ -116,7 +116,9 @@ var Filmstrip = (function() {
frame.displayImage(item.blob, item.width, item.height, item.preview);
}
else if (item.isVideo) {
- frame.displayVideo(item.blob, item.width, item.height, item.rotation);
+ frame.displayVideo(item.blob, item.poster,
+ item.width, item.height,
+ item.rotation);
}
preview.classList.remove('offscreen');
@@ -328,18 +330,17 @@ var Filmstrip = (function() {
offscreenImage.src = URL.createObjectURL(previewBlob);
offscreenImage.onload = function() {
- createThumbnailFromElement(offscreenImage, false, 0,
- function(thumbnail) {
- addItem({
- isImage: true,
- filename: filename,
- thumbnail: thumbnail,
- blob: blob,
- width: metadata.width,
- height: metadata.height,
- preview: metadata.preview
- });
- });
+ createThumbnailFromImage(offscreenImage, function(thumbnail) {
+ addItem({
+ isImage: true,
+ filename: filename,
+ thumbnail: thumbnail,
+ blob: blob,
+ width: metadata.width,
+ height: metadata.height,
+ preview: metadata.preview
+ });
+ });
URL.revokeObjectURL(offscreenImage.src);
offscreenImage.onload = null;
offscreenImage.src = null;
@@ -378,18 +379,19 @@ var Filmstrip = (function() {
}
offscreenVideo.onloadedmetadata = function() {
- createThumbnailFromElement(offscreenVideo, true, rotation,
- function(thumbnail) {
- addItem({
- isVideo: true,
- filename: filename,
- thumbnail: thumbnail,
- blob: blob,
- width: offscreenVideo.videoWidth,
- height: offscreenVideo.videoHeight,
- rotation: rotation
- });
+ createThumbnailFromVideo(offscreenVideo, rotation, filename,
+ function(thumbnail, poster) {
+ addItem({
+ isVideo: true,
+ filename: filename,
+ thumbnail: thumbnail,
+ poster: poster,
+ blob: blob,
+ width: offscreenVideo.videoWidth,
+ height: offscreenVideo.videoHeight,
+ rotation: rotation
});
+ });
URL.revokeObjectURL(url);
offscreenVideo.onerror = null;
offscreenVideo.onloadedmetadata = null;
@@ -444,16 +446,16 @@ var Filmstrip = (function() {
// Create a thumbnail size canvas, copy the <img> or <video> into it
// cropping the edges as needed to make it fit, and then extract the
// thumbnail image as a blob and pass it to the callback.
- function createThumbnailFromElement(elt, video, rotation, callback) {
+ function createThumbnailFromImage(img, callback) {
// Create a thumbnail image
var canvas = document.createElement('canvas');
var context = canvas.getContext('2d');
canvas.width = THUMBNAIL_WIDTH;
canvas.height = THUMBNAIL_HEIGHT;
- var eltwidth = video ? elt.videoWidth : elt.width;
- var eltheight = video ? elt.videoHeight : elt.height;
- var scalex = canvas.width / eltwidth;
- var scaley = canvas.height / eltheight;
+ var imgwidth = img.width;
+ var imgheight = img.height;
+ var scalex = canvas.width / imgwidth;
+ var scaley = canvas.height / imgheight;
// Take the larger of the two scales: we crop the image to the thumbnail
var scale = Math.max(scalex, scaley);
@@ -462,69 +464,116 @@ var Filmstrip = (function() {
// canvas to create the thumbnail
var w = Math.round(THUMBNAIL_WIDTH / scale);
var h = Math.round(THUMBNAIL_HEIGHT / scale);
- var x = Math.round((eltwidth - w) / 2);
- var y = Math.round((eltheight - h) / 2);
+ var x = Math.round((imgwidth - w) / 2);
+ var y = Math.round((imgheight - h) / 2);
+
+ // Draw that region of the image into the canvas, scaling it down
+ context.drawImage(img, x, y, w, h,
+ 0, 0, THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT);
+
+ canvas.toBlob(callback, 'image/jpeg');
+ }
+
+ // Create a thumbnail size canvas, copy the <img> or <video> into it
+ // cropping the edges as needed to make it fit, and then extract the
+ // thumbnail image as a blob and pass it to the callback.
+ function createThumbnailFromVideo(video, rotation, filename, callback) {
+ var videowidth = video.videoWidth;
+ var videoheight = video.videoHeight;
+
+ // First, create a full-size unrotated poster image
+ var postercanvas = document.createElement('canvas');
+ var postercontext = postercanvas.getContext('2d');
+ postercanvas.width = videowidth;
+ postercanvas.height = videoheight;
+ postercontext.drawImage(video, 0, 0);
+
+ // Now create a thumbnail
+ var thumbnailcanvas = document.createElement('canvas');
+ var thumbnailcontext = thumbnailcanvas.getContext('2d');
+ thumbnailcanvas.width = THUMBNAIL_WIDTH;
+ thumbnailcanvas.height = THUMBNAIL_HEIGHT;
+
+ var scalex = THUMBNAIL_WIDTH / videowidth;
+ var scaley = THUMBNAIL_HEIGHT / videoheight;
+
+ // Take the larger of the two scales: we crop the image to the thumbnail
+ var scale = Math.max(scalex, scaley);
+
+ // Calculate the region of the image that will be copied to the
+ // canvas to create the thumbnail
+ var w = Math.round(THUMBNAIL_WIDTH / scale);
+ var h = Math.round(THUMBNAIL_HEIGHT / scale);
+ var x = Math.round((videowidth - w) / 2);
+ var y = Math.round((videoheight - h) / 2);
// If a rotation is specified, rotate the canvas context
if (rotation) {
- context.save();
+ thumbnailcontext.save();
switch (rotation) {
case 90:
- context.translate(THUMBNAIL_WIDTH, 0);
- context.rotate(Math.PI / 2);
+ thumbnailcontext.translate(THUMBNAIL_WIDTH, 0);
+ thumbnailcontext.rotate(Math.PI / 2);
break;
case 180:
- context.translate(THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT);
- context.rotate(Math.PI);
+ thumbnailcontext.translate(THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT);
+ thumbnailcontext.rotate(Math.PI);
break;
case 270:
- context.translate(0, THUMBNAIL_HEIGHT);
- context.rotate(-Math.PI / 2);
+ thumbnailcontext.translate(0, THUMBNAIL_HEIGHT);
+ thumbnailcontext.rotate(-Math.PI / 2);
break;
}
}
- // Draw that region of the image into the canvas, scaling it down
- context.drawImage(elt, x, y, w, h,
- 0, 0, THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT);
+ // Draw that region of the poster into the thumbnail, scaling it down
+ thumbnailcontext.drawImage(postercanvas, x, y, w, h,
+ 0, 0, THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT);
// Restore the default rotation so the play arrow comes out correctly
if (rotation) {
- context.restore();
+ thumbnailcontext.restore();
}
// If this is a video, superimpose a translucent play button over
- // the captured video frame to distinguish it from a still photo thumbnail
- if (video) {
- // First draw a transparent gray circle
- context.fillStyle = 'rgba(0, 0, 0, .3)';
- context.beginPath();
- context.arc(THUMBNAIL_WIDTH / 2, THUMBNAIL_HEIGHT / 2,
- THUMBNAIL_HEIGHT / 3, 0, 2 * Math.PI, false);
- context.fill();
-
- // Now outline the circle in white
- context.strokeStyle = 'rgba(255,255,255,.6)';
- context.lineWidth = 2;
- context.stroke();
-
- // And add a white play arrow.
- context.beginPath();
- context.fillStyle = 'rgba(255,255,255,.6)';
- // The height of an equilateral triangle is sqrt(3)/2 times the side
- var side = THUMBNAIL_HEIGHT / 3;
- var triangle_height = side * Math.sqrt(3) / 2;
- context.moveTo(THUMBNAIL_WIDTH / 2 + triangle_height * 2 / 3,
- THUMBNAIL_HEIGHT / 2);
- context.lineTo(THUMBNAIL_WIDTH / 2 - triangle_height / 3,
- THUMBNAIL_HEIGHT / 2 - side / 2);
- context.lineTo(THUMBNAIL_WIDTH / 2 - triangle_height / 3,
- THUMBNAIL_HEIGHT / 2 + side / 2);
- context.closePath();
- context.fill();
+ // the captured video frame to distinguish it from a still photo
+ // thumbnail. First draw a transparent gray circle
+ thumbnailcontext.fillStyle = 'rgba(0, 0, 0, .3)';
+ thumbnailcontext.beginPath();
+ thumbnailcontext.arc(THUMBNAIL_WIDTH / 2, THUMBNAIL_HEIGHT / 2,
+ THUMBNAIL_HEIGHT / 3, 0, 2 * Math.PI, false);
+ thumbnailcontext.fill();
+
+ // Now outline the circle in white
+ thumbnailcontext.strokeStyle = 'rgba(255,255,255,.6)';
+ thumbnailcontext.lineWidth = 2;
+ thumbnailcontext.stroke();
+
+ // And add a white play arrow.
+ thumbnailcontext.beginPath();
+ thumbnailcontext.fillStyle = 'rgba(255,255,255,.6)';
+ // The height of an equilateral triangle is sqrt(3)/2 times the side
+ var side = THUMBNAIL_HEIGHT / 3;
+ var triangle_height = side * Math.sqrt(3) / 2;
+ thumbnailcontext.moveTo(THUMBNAIL_WIDTH / 2 + triangle_height * 2 / 3,
+ THUMBNAIL_HEIGHT / 2);
+ thumbnailcontext.lineTo(THUMBNAIL_WIDTH / 2 - triangle_height / 3,
+ THUMBNAIL_HEIGHT / 2 - side / 2);
+ thumbnailcontext.lineTo(THUMBNAIL_WIDTH / 2 - triangle_height / 3,
+ THUMBNAIL_HEIGHT / 2 + side / 2);
+ thumbnailcontext.closePath();
+ thumbnailcontext.fill();
+
+ // Save the poster image to storage, then call the callback
+ postercanvas.toBlob(savePosterImage, 'image/jpeg');
+
+ // The Gallery app depends on this poster image being saved here
+ function savePosterImage(poster) {
+ Camera._pictureStorage.addNamed(poster, filename.replace('.3gp', '.jpg'));
+ thumbnailcanvas.toBlob(function(thumbnail) {
+ callback(thumbnail, poster);
+ }, 'image/jpeg');
}
-
- canvas.toBlob(callback, 'image/jpeg');
}
function setOrientation(orientation) {
View
2 apps/camera/style/VideoPlayer.css
@@ -1,5 +1,5 @@
/* styles for the video element itself */
-.videoPlayer {
+.videoPoster, .videoPlayer {
position: absolute;
left: 0; /* we position it with a transform */
top:0;
View
129 apps/gallery/js/MetadataParser.js
@@ -7,7 +7,7 @@
//
// This file depends on JPEGMetadataParser.js and blobview.js
//
-var metadataParsers = (function() {
+var metadataParser = (function() {
// If we generate our own thumbnails, aim for this size
var THUMBNAIL_WIDTH = 120;
var THUMBNAIL_HEIGHT = 120;
@@ -18,9 +18,8 @@ var metadataParsers = (function() {
// Don't try to open images with more pixels than this
var MAX_IMAGE_PIXEL_SIZE = 5 * 1024 * 1024; // 5 megapixels
- // <img> and <video> elements for loading images and videos
+ // An <img> element for loading images
var offscreenImage = new Image();
- var offscreenVideo = document.createElement('video');
// Create a thumbnail size canvas, copy the <img> or <video> into it
// cropping the edges as needed to make it fit, and then extract the
@@ -32,8 +31,8 @@ var metadataParsers = (function() {
var context = canvas.getContext('2d');
canvas.width = THUMBNAIL_WIDTH;
canvas.height = THUMBNAIL_HEIGHT;
- var eltwidth = video ? elt.videoWidth : elt.width;
- var eltheight = video ? elt.videoHeight : elt.height;
+ var eltwidth = elt.width;
+ var eltheight = elt.height;
var scalex = canvas.width / eltwidth;
var scaley = canvas.height / eltheight;
@@ -106,17 +105,19 @@ var metadataParsers = (function() {
context.fill();
}
- canvas.toBlob(function(blob) {
- // This setTimeout is here in the hopes that it gives gecko a bit
- // of time to release the memory that holds the decoded image before
- // we start creating the next thumbnail.
- setTimeout(function() {
- callback(blob);
- });
- }, 'image/jpeg');
+ canvas.toBlob(callback, 'image/jpeg');
}
- function imageMetadataParser(file, metadataCallback, metadataError) {
+ var VIDEOFILE = /DCIM\/\d{3}MZLLA\/VID_\d{4}\.jpg/;
+
+ function metadataParser(file, metadataCallback, metadataError) {
+ // If the file is a poster image for a video file, then we've want
+ // video metadata, not image metadata
+ if (VIDEOFILE.test(file.name)) {
+ videoMetadataParser(file, metadataCallback, metadataError);
+ return;
+ }
+
if (file.type !== 'image/jpeg') {
// For any kind of image other than JPEG, we just have to get
// our metadata with an <img> tag
@@ -228,76 +229,54 @@ var metadataParsers = (function() {
}
function videoMetadataParser(file, metadataCallback, errorCallback) {
- try {
- if (file.type && !offscreenVideo.canPlayType(file.type)) {
- errorCallback("can't play video file type: " + file.type);
- return;
- }
+ var metadata = {};
+ var videofilename = file.name.replace('.jpg', '.3gp');
+ metadata.video = videofilename;
+ var getreq = videostorage.get(videofilename);
+ getreq.onerror = function() {
+ errorCallback('cannot get video file: ' + videofilename);
+ }
+ getreq.onsuccess = function() {
+ var videofile = getreq.result;
+ getVideoRotation(videofile, function(rotation) {
+ if (typeof rotation === 'number') {
+ metadata.rotation = rotation;
+ getVideoThumbnailAndSize();
+ }
+ else if (typeof rotation === 'string') {
+ errorCallback('Video rotation:', rotation);
+ }
+ });
+ }
+
+ function getVideoThumbnailAndSize() {
var url = URL.createObjectURL(file);
+ offscreenImage.src = url;
- offscreenVideo.preload = 'metadata';
- offscreenVideo.style.width = THUMBNAIL_WIDTH + 'px';
- offscreenVideo.style.height = THUMBNAIL_HEIGHT + 'px';
- offscreenVideo.src = url;
+ offscreenImage.onerror = function() {
+ URL.revokeObjectURL(url);
+ offscreenImage.removeAttribute('src');
+ errorCallback('getVideoThumanailAndSize: Image failed to load');
+ };
- offscreenVideo.onerror = function() {
+ offscreenImage.onload = function() {
URL.revokeObjectURL(url);
- offscreenVideo.onerror = null;
- offscreenVideo.src = null;
- errorCallback('not a video file');
- }
- offscreenVideo.onloadedmetadata = function() {
- var metadata = {};
- metadata.video = true;
- metadata.duration = offscreenVideo.duration;
- metadata.width = offscreenVideo.videoWidth;
- metadata.height = offscreenVideo.videoHeight;
- metadata.rotation = 0;
-
- // 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.
- // getVideoRotation is defined in shared/js/media/get_video_rotation.js
- if (file.name.substring(file.name.lastIndexOf('.') + 1) === '3gp') {
- getVideoRotation(file, function(rotation) {
- if (typeof rotation === 'number')
- metadata.rotation = rotation;
- else if (typeof rotation === 'string')
- console.warn('Video rotation:', rotation);
- createThumbnail();
- });
- }
- else {
- createThumbnail();
- }
+ // We store the unrotated size of the poster image, which we
+ // require to have the same size and rotation as the video
+ metadata.width = offscreenImage.width;
+ metadata.height = offscreenImage.height;
- function createThumbnail() {
- offscreenVideo.currentTime = 1; // read 1 second into video
- offscreenVideo.onseeked = function onseeked() {
- createThumbnailFromElement(offscreenVideo, true, metadata.rotation,
- function(thumbnail) {
- URL.revokeObjectURL(url);
- offscreenVideo.onerror = null;
- offscreenVideo.onseeked = null;
- offscreenVideo.removeAttribute('src');
- offscreenVideo.load();
- metadata.thumbnail = thumbnail;
- metadataCallback(metadata);
- });
- };
- }
+ createThumbnailFromElement(offscreenImage, true, metadata.rotation,
+ function(thumbnail) {
+ metadata.thumbnail = thumbnail;
+ offscreenImage.removeAttribute('src');
+ metadataCallback(metadata);
+ });
};
}
- catch (e) {
- console.error('Exception in videoMetadataParser', e, e.stack);
- errorCallback('Exception in videoMetadataParser');
- }
}
- return {
- imageMetadataParser: imageMetadataParser,
- videoMetadataParser: videoMetadataParser
- };
+ return metadataParser;
}());
View
43 apps/gallery/js/frames.js
@@ -293,22 +293,25 @@ function setupFrameContent(n, frame) {
// Remember what file we're going to display
frame.filename = fileinfo.name;
- if (fileinfo.metadata.video) {
- videodb.getFile(fileinfo.name, function(file) {
- frame.displayVideo(file,
- fileinfo.metadata.width,
- fileinfo.metadata.height,
- fileinfo.metadata.rotation || 0);
- });
- }
- else {
- photodb.getFile(fileinfo.name, function(file) {
- frame.displayImage(file,
+ photodb.getFile(fileinfo.name, function(imagefile) {
+ if (fileinfo.metadata.video) {
+ // If this is a video, then the file we just got is the poster image
+ // and we still have to fetch the actual video
+ getVideoFile(fileinfo.metadata.video, function(videofile) {
+ frame.displayVideo(videofile, imagefile,
+ fileinfo.metadata.width,
+ fileinfo.metadata.height,
+ fileinfo.metadata.rotation || 0);
+ });
+ }
+ else {
+ // Otherwise, just display the image
+ frame.displayImage(imagefile,
fileinfo.metadata.width,
fileinfo.metadata.height,
fileinfo.metadata.preview);
- });
- }
+ }
+ });
}
var FRAME_BORDER_WIDTH = 3;
@@ -356,9 +359,10 @@ function nextFile(time) {
if (currentFileIndex === files.length - 1)
return;
- // Don't pan a playing video!
- if (currentFrame.displayingVideo && !currentFrame.video.player.paused)
- currentFrame.video.pause();
+ // If the current frame is using a <video> element instead of just
+ // displaying a poster image, reset it back to just the image
+ if (currentFrame.displayingVideo && currentFrame.video.playerShowing)
+ currentFrame.video.init();
// Set a flag to ignore pan and zoom gestures during the transition.
transitioning = true;
@@ -405,9 +409,10 @@ function previousFile(time) {
if (currentFileIndex === 0)
return;
- // Don't pan a playing video!
- if (currentFrame.displayingVideo && !currentFrame.video.player.paused)
- currentFrame.video.pause();
+ // If the current frame is using a <video> element instead of just
+ // displaying a poster image, reset it back to just the image.
+ if (currentFrame.displayingVideo && currentFrame.video.playerShowing)
+ currentFrame.video.init();
// Set a flag to ignore pan and zoom gestures during the transition.
transitioning = true;
View
199 apps/gallery/js/gallery.js
@@ -82,9 +82,13 @@ var editedPhotoIndex;
var selectedFileNames = [];
var selectedFileNamesToBlobs = {};
-// The MediaDB objects that manage the filesystem and the database of metadata
-// See init()
-var photodb, videodb;
+// The MediaDB object that manages the filesystem and the database of metadata
+var photodb;
+
+// We manage videos through their poster images, which are photos and so get
+// listed in the photodb above. But when we need to access the actual video
+// file, we have to get that from a device storage object for videos.
+var videostorage;
var visibilityMonitor;
@@ -175,7 +179,7 @@ function init() {
// Initialize MediaDB objects for photos and videos, and set up their
// event handlers.
function initDB(include_videos) {
- photodb = new MediaDB('pictures', imageMetadataParser, {
+ photodb = new MediaDB('pictures', metadataParserWrapper, {
mimeTypes: ['image/jpeg', 'image/png'],
version: 2,
autoscan: false, // We're going to call scan() explicitly
@@ -184,40 +188,19 @@ function initDB(include_videos) {
});
if (include_videos) {
- // For videos, this app is only interested in files under DCIM/.
- videodb = new MediaDB('videos', videoMetadataParser, {
- directory: 'DCIM/',
- autoscan: false, // We're going to call scan() explicitly
- batchHoldTime: 150, // Batch files during scanning
- batchSize: PAGE_SIZE // Max batch size: one screenful
- });
- }
- else {
- videodb = null;
+ videostorage = navigator.getDeviceStorage('videos');
}
var loaded = false;
- function imageMetadataParser(file, onsuccess, onerror) {
- if (loaded) {
- metadataParsers.imageMetadataParser(file, onsuccess, onerror);
- return;
- }
-
- loadScript('js/metadata_scripts.js', function() {
- loaded = true;
- metadataParsers.imageMetadataParser(file, onsuccess, onerror);
- });
- }
-
- function videoMetadataParser(file, onsuccess, onerror) {
+ function metadataParserWrapper(file, onsuccess, onerror) {
if (loaded) {
- metadataParsers.videoMetadataParser(file, onsuccess, onerror);
+ metadataParser(file, onsuccess, onerror);
return;
}
loadScript('js/metadata_scripts.js', function() {
loaded = true;
- metadataParsers.videoMetadataParser(file, onsuccess, onerror);
+ metadataParser(file, onsuccess, onerror);
});
}
@@ -239,46 +222,19 @@ function initDB(include_videos) {
if (currentOverlay === 'nocard' || currentOverlay === 'pluggedin')
showOverlay(null);
- // If we're including videos also, be sure that they are ready
- if (include_videos) {
- if (videodb.state === MediaDB.READY)
- initThumbnails();
- }
- else {
- initThumbnails();
- }
+ initThumbnails(include_videos);
};
- if (include_videos) {
- videodb.onready = function() {
- // If the photodb is also ready, create thumbnails.
- // Depending on the order of the ready events, either this code
- // or the code above will fire and set up the thumbnails
- if (photodb.state === MediaDB.READY)
- initThumbnails();
- };
- }
-
- // When the mediadbs are scanning, let the user know. We count scan starts
- // and ends so we correctly display the throbber while either db is scanning.
- var scanning = 0;
-
photodb.onscanstart = function onscanstart() {
- scanning++;
- if (scanning == 1) {
- // Show the scanning indicator
- $('progress').classList.remove('hidden');
- $('throbber').classList.add('throb');
- }
+ // Show the scanning indicator
+ $('progress').classList.remove('hidden');
+ $('throbber').classList.add('throb');
};
photodb.onscanend = function onscanend() {
- scanning--;
- if (scanning == 0) {
- // Hide the scanning indicator
- $('progress').classList.add('hidden');
- $('throbber').classList.remove('throb');
- }
+ // Hide the scanning indicator
+ $('progress').classList.add('hidden');
+ $('throbber').classList.remove('throb');
};
// One or more files was created (or was just discovered by a scan)
@@ -290,12 +246,17 @@ function initDB(include_videos) {
photodb.ondeleted = function(event) {
event.detail.forEach(fileDeleted);
};
+}
- if (include_videos) {
- videodb.onscanstart = photodb.onscanstart;
- videodb.onscanend = photodb.onscanend;
- videodb.oncreated = photodb.oncreated;
- videodb.ondeleted = photodb.ondeleted;
+// Pass the filename of the poster image and get the video file back
+function getVideoFile(filename, callback) {
+ // We get videos directly through the video device storage
+ var req = videostorage.get(filename);
+ req.onsuccess = function() {
+ callback(req.result);
+ };
+ req.onerror = function() {
+ console.error('Failed to get video file', filename);
}
}
@@ -310,15 +271,15 @@ function compareFilesByDate(a, b) {
}
//
-// Enumerate existing entries in the photo and video databases in reverse
+// Enumerate existing entries in the media database in reverse
// chronological order (most recent first) and display thumbnails for them all.
// After the thumbnails are displayed, scan for new files.
//
// This function gets called when the app first starts up, and also
// when the sdcard becomes available again after a USB mass storage
// session or an sdcard replacement.
//
-function initThumbnails() {
+function initThumbnails(include_videos) {
// If we've already been called once, then we've already got thumbnails
// displayed. There is no need to re-enumerate them, so we just go
// straight to scanning for new files
@@ -343,63 +304,29 @@ function initThumbnails() {
// from most recent to least recent.
// Temporary arrays to hold enumerated files
- var photos = [], videos = [], interleaved = [];
+ var batch = [];
var batchsize = PAGE_SIZE;
photodb.enumerate('date', null, 'prev', function(fileinfo) {
- photos.push(fileinfo);
- merge();
- });
-
- if (videodb) {
- videodb.enumerate('date', null, 'prev', function(fileinfo) {
- videos.push(fileinfo);
- merge();
- });
- }
- else {
- videos.push(null); // This means we're done enumerating videos
- }
-
- // Create thumbnails for as many of the files in the photos and videos arrays
- // as we can. This is the tricky bit of the algorithm for ensuring that
- // they are sorted by date
- function merge() {
- // If we don't have at least one of each, we don't know what the newest is
- while (photos.length > 0 && videos.length > 0) {
- if (photos[0] === null && videos[0] === null) {
- // Both enumerations are done
- done();
- break;
- }
-
- // If we've finished enumerating photos, then videos[0] is next
- if (photos[0] === null) {
- batch(videos.shift());
- }
- else if (videos[0] === null) {
- batch(photos.shift());
- }
- else if (videos[0].date > photos[0].date) {
- batch(videos.shift());
- }
- else {
- batch(photos.shift());
+ if (fileinfo) {
+ // For a pick activity, don't display videos
+ if (!include_videos && fileinfo.metadata.video)
+ return;
+
+ batch.push(fileinfo);
+ if (batch.length >= batchsize) {
+ flush();
+ batchsize *= 2;
}
}
- }
-
- function batch(fileinfo) {
- interleaved.push(fileinfo);
- if (interleaved.length >= batchsize) {
- flush();
- batchsize *= 2;
+ else {
+ done();
}
- }
+ });
function flush() {
- interleaved.forEach(thumb);
- interleaved.length = 0;
+ batch.forEach(thumb);
+ batch.length = 0;
}
function thumb(fileinfo) {
@@ -415,16 +342,10 @@ function initThumbnails() {
}
// Now that we've enumerated all the photos and videos we already know
// about go start looking for new photos and videos.
- scan();
+ photodb.scan();
}
}
-function scan() {
- photodb.scan();
- if (videodb)
- videodb.scan();
-}
-
function fileDeleted(filename) {
// Find the deleted file in our files array
for (var n = 0; n < files.length; n++) {
@@ -485,10 +406,13 @@ function deleteFile(n) {
// deletes the file in device storage. This will generate an change
// event which will call imageDeleted()
var fileinfo = files[n];
- if (fileinfo.metadata.video)
- videodb.deleteFile(fileinfo.name);
- else
- photodb.deleteFile(files[n].name);
+ photodb.deleteFile(files[n].name);
+
+ // If it is a video, however, we can't just delete the poster image, but
+ // must also delete the video file.
+ if (fileinfo.metadata.video) {
+ videostorage.delete(fileinfo.metadata.video);
+ }
}
function fileCreated(fileinfo) {
@@ -822,10 +746,17 @@ function updateSelection(thumbnail) {
if (selected) {
selectedFileNames.push(filename);
- var db = files[index].metadata.video ? videodb : photodb;
- db.getFile(filename, function(file) {
- selectedFileNamesToBlobs[filename] = file;
- });
+ if (files[index].metadata.video) {
+ getVideoFile(files[index].metadata.video, function(file) {
+ selectedFileNamesToBlobs[filename] = file;
+ });
+ }
+ else {
+ // We get photos through the photo db
+ photodb.getFile(filename, function(file) {
+ selectedFileNamesToBlobs[filename] = file;
+ });
+ }
}
else {
delete selectedFileNamesToBlobs[filename];
View
1 apps/gallery/manifest.webapp
@@ -11,6 +11,7 @@
"permissions": {
"device-storage:pictures":{ "access": "readwrite" },
"device-storage:videos":{ "access": "readwrite" },
+ "deprecated-hwvideo":{},
"audio-channel-content":{},
"settings":{ "access": "readonly" }
},
View
4 apps/gallery/style/VideoPlayer.css
@@ -1,5 +1,5 @@
-/* styles for the video element itself */
-.videoPlayer {
+/* styles for the video player and poster image */
+.videoPoster, .videoPlayer {
position: absolute;
left: 0; /* we position it with a transform */
top:0;
View
89 apps/video/js/video.js
@@ -48,6 +48,9 @@ var dragging = false;
var fullscreenTimer;
var fullscreenCallback;
+// Videos recorded by our own camera have filenames of this form
+var FROMCAMERA = /^DCIM\/\d{3}MZLLA\/VID_\d{4}\.3gp$/;
+
function init() {
videodb = new MediaDB('videos', metaDataParser);
@@ -162,6 +165,17 @@ dom.thumbnails.addEventListener('contextmenu', function(evt) {
function deleteFile(file) {
var msg = navigator.mozL10n.get('confirm-delete');
if (confirm(msg + ' ' + file)) {
+ if (FROMCAMERA.test(file)) {
+ // If we're deleting a video file recorded by our camera,
+ // we also need to delete the poster image associated with
+ // that video.
+ var postername = file.replace('.3gp', '.jpg');
+ navigator.getDeviceStorage('pictures').delete(postername);
+ }
+
+ // Whether or not there was a poster file to delete, delete the
+ // actual video file. This will cause the MediaDB to send a 'deleted'
+ // event, and the handler for that event will call videoDeleted() below.
videodb.deleteFile(file);
}
}
@@ -196,7 +210,26 @@ function updateDialog() {
}
}
-function metaDataParser(videofile, callback, metadataError) {
+function metaDataParser(videofile, callback, metadataError, delayed) {
+ // XXX
+ // When the camera records a video, it saves the video file and then
+ // uses a <video> tag to create a poster image for that video.
+ // But if the Video app is running, we get an event from device storage
+ // and start parsing the metadata when the video file is created. So now
+ // the Camera app and the Video app are both trying to use the video
+ // decoding hardware at the same time. The camera app really has to
+ // succeed. We should modify this app to wait for and use the poster image
+ // the way that the Gallery app does. For now, however, we avoid the problem
+ // by just waiting to give the Camera app time to save the poster image.
+ // In the worst case, we could fail to parse the metadata here. But that
+ // is better than having the camera fail to record the video correctly.
+ //
+ if (!delayed && FROMCAMERA.test(videofile.name)) {
+ setTimeout(function() {
+ metaDataParser(videofile, callback, metadataError, true);
+ }, 2000);
+ return;
+ }
var previewPlayer = document.createElement('video');
var completed = false;
@@ -219,6 +252,8 @@ function metaDataParser(videofile, callback, metadataError) {
if (!completed) {
metadataError(metadata.title);
}
+ previewPlayer.removeAttribute('src');
+ previewPlayer.load();
};
previewPlayer.onloadedmetadata = function() {
@@ -326,8 +361,8 @@ function setPosterImage(dom, poster) {
if (dom.dataset.uri) {
URL.revokeObjectURL(dom.dataset.uri);
}
- dom.dataset.uri = URL.createObjectURL(poster)
- dom.style.backgroundImage = 'url('+dom.dataset.uri+')';
+ dom.dataset.uri = URL.createObjectURL(poster);
+ dom.style.backgroundImage = 'url(' + dom.dataset.uri + ')';
}
function showOverlay(id) {
@@ -496,7 +531,7 @@ function showPlayer(data, autoPlay) {
playerShowing = true;
setPlayerSize();
- if ('name' in currentVideo && /^DCIM/.test(currentVideo.name)) {
+ if ('name' in currentVideo && FROMCAMERA.test(currentVideo.name)) {
dom.deleteVideoButton.classList.remove('hidden');
}
@@ -533,6 +568,14 @@ function hidePlayer() {
dom.thumbnails.classList.remove('hidden');
playerShowing = false;
updateDialog();
+
+ // Unload the video. This releases the video decoding hardware
+ // so other apps can use it. Note that any time the video app is hidden
+ // (by switching to another app) we leave fullscreen mode, and this
+ // code gets triggered, so if the video app is not visible it should
+ // not be holding on to the video hardware
+ dom.player.removeAttribute('src');
+ dom.player.load();
}
if (!('metadata' in currentVideo)) {
@@ -886,13 +929,43 @@ document.addEventListener('mozfullscreenchange', function() {
// Pause on visibility change
document.addEventListener('mozvisibilitychange', function visibilityChange() {
- if (document.mozHidden && playing) {
- pause();
- } else if (!document.mozHidden && document.mozFullScreenElement) {
- setControlsVisibility(true);
+ if (document.mozHidden) {
+ if (playing)
+ pause();
+
+ if (playerShowing)
+ releaseVideo();
+ }
+ else {
+ if (document.mozFullScreenElement)
+ setControlsVisibility(true);
+
+ if (playerShowing)
+ restoreVideo();
}
});
+// This app uses deprecated-hwvideo permission to access video decoding hardware
+// But Camera and Gallery also need to use that hardware, and those three apps
+// may only have one video playing at a time among them. So we need to be
+// careful to relinquish the hardware when we are not visible.
+
+var restoreTime;
+
+// Call this when the app is hidden
+function releaseVideo() {
+ restoreTime = dom.player.currentTime;
+ dom.player.removeAttribute('src');
+ dom.player.load();
+}
+
+// Call this when the app becomes visible again
+function restoreVideo() {
+ setVideoUrl(dom.player, currentVideo, function() {
+ dom.player.currentTime = restoreTime;
+ });
+}
+
// show|hide controls over the player
dom.videoControls.addEventListener('mousedown', playerMousedown);
View
1 apps/video/manifest.webapp
@@ -8,6 +8,7 @@
"url": "https://github.com/mozilla-b2g/gaia"
},
"permissions": {
+ "device-storage:pictures":{ "access": "readwrite" },
"device-storage:videos":{ "access": "readwrite" },
"settings":{ "access": "readonly" },
"deprecated-hwvideo":{},
View
53 shared/js/media/media_frame.js
@@ -40,7 +40,9 @@ function MediaFrame(container, includeVideo) {
}
this.displayingVideo = false;
this.displayingImage = false;
- this.blob = null;
+ this.imageblob = null;
+ this.videoblob = null;
+ this.posterblob = null;
this.url = null;
}
@@ -50,7 +52,7 @@ MediaFrame.prototype.displayImage = function displayImage(blob, width, height,
this.clear(); // Reset everything
// Remember what we're displaying
- this.blob = blob;
+ this.imageblob = blob;
this.fullsizeWidth = width;
this.fullsizeHeight = height;
this.preview = preview;
@@ -190,23 +192,24 @@ MediaFrame.prototype._displayImage = function _displayImage(blob, width, height,
MediaFrame.prototype._switchToFullSizeImage = function _switchToFull(cb) {
if (this.displayingImage && this.displayingPreview) {
this.displayingPreview = false;
- this._displayImage(this.blob, this.fullsizeWidth, this.fullsizeHeight,
+ this._displayImage(this.imageblob, this.fullsizeWidth, this.fullsizeHeight,
true, cb);
}
};
MediaFrame.prototype._switchToPreviewImage = function _switchToPreview() {
if (this.displayingImage && !this.displayingPreview) {
this.displayingPreview = true;
- this._displayImage(this.blob.slice(this.preview.start,
- this.preview.end,
- 'image/jpeg'),
+ this._displayImage(this.imageblob.slice(this.preview.start,
+ this.preview.end,
+ 'image/jpeg'),
this.preview.width,
this.preview.height);
}
};
-MediaFrame.prototype.displayVideo = function displayVideo(blob, width, height,
+MediaFrame.prototype.displayVideo = function displayVideo(videoblob, posterblob,
+ width, height,
rotation)
{
if (!this.video)
@@ -217,19 +220,21 @@ MediaFrame.prototype.displayVideo = function displayVideo(blob, width, height,
// Keep track of what kind of content we have
this.displayingVideo = true;
- // Show the video player and hide the image
- this.video.show();
-
- // Remember the blob
- this.blob = blob;
+ // Remember the blobs
+ this.videoblob = videoblob;
+ this.posterblob = posterblob;
- // Get a new URL for this blob
- this.url = URL.createObjectURL(blob);
+ // Get new URLs for the blobs
+ this.videourl = URL.createObjectURL(videoblob);
+ this.posterurl = URL.createObjectURL(posterblob);
- // Display it in the video element.
+ // Display them in the video element.
// The VideoPlayer class takes care of positioning itself, so we
// don't have to do anything here with computeFit() or setPosition()
- this.video.load(this.url, rotation || 0);
+ this.video.load(this.videourl, this.posterurl, width, height, rotation || 0);
+
+ // Show the player controls
+ this.video.show();
};
// Reset the frame state, release any urls and and hide everything
@@ -239,7 +244,9 @@ MediaFrame.prototype.clear = function clear() {
this.displayingPreview = false;
this.displayingVideo = false;
this.itemWidth = this.itemHeight = null;
- this.blob = null;
+ this.imageblob = null;
+ this.videoblob = null;
+ this.posterblob = null;
this.fullsizeWidth = this.fullsizeHeight = null;
this.preview = null;
this.fit = null;
@@ -254,14 +261,12 @@ MediaFrame.prototype.clear = function clear() {
// Hide the video player
if (this.video) {
+ this.video.reset();
this.video.hide();
-
- // If the video player has its src set, clear it and release resources
- // We do this in a roundabout way to avoid getting a warning in the console
- if (this.video.player.src) {
- this.video.player.removeAttribute('src');
- this.video.player.load();
- }
+ if (this.videourl)
+ URL.revokeObjectURL(this.videourl);
+ if (this.posterurl)
+ URL.revokeObjectURL(this.posterurl);
}
};
View
170 shared/js/media/video_player.js
@@ -1,8 +1,22 @@
'use strict';
+//
// Create a <video> element and <div> containing a video player UI and
// add them to the specified container. The UI requires a GestureDetector
// to be running for the container or one of its ancestors.
+//
+// Some devices have only a single hardware video decoder and can only
+// have one video tag playing anywhere at once. So this class is careful
+// to only load content into a <video> element when the user really wants
+// to play it. At other times it displays a poster image for the video.
+// Initially, it displays the poster image. Pressing play starts the video.
+// Pausing pauses the video but does not revert to the poster. Finishing the
+// video reverts to the initial state with the poster image displayed.
+// If we get a visiblitychange event saying that we've been hidden, we
+// remember the playback position, pause the video take a temporary
+// screenshot and display it, and unload the video. If shown again
+// and if the user clicks play again, we resume the video where we left off.
+//
function VideoPlayer(container) {
if (typeof container === 'string')
container = document.getElementById(container);
@@ -16,6 +30,7 @@ function VideoPlayer(container) {
}
// This copies the controls structure of the Video app
+ var poster = newelt(container, 'img', 'videoPoster');
var player = newelt(container, 'video', 'videoPlayer');
var controls = newelt(container, 'div', 'videoPlayerControls');
var playbutton = newelt(controls, 'button', 'videoPlayerPlayButton');
@@ -29,32 +44,100 @@ function VideoPlayer(container) {
var playHead = newelt(progress, 'div', 'videoPlayerPlayHead');
var durationText = newelt(slider, 'span', 'videoPlayerDurationText');
+ this.poster = poster;
this.player = player;
this.controls = controls;
player.preload = 'metadata';
+ player.mozAudioChannelType = 'content';
var self = this;
var controlsHidden = false;
var dragging = false;
var pausedBeforeDragging = false;
var screenLock; // keep the screen on when playing
var endedTimer;
+ var videourl; // the url of the video to play
+ var posterurl; // the url of the poster image to display
var rotation; // Do we have to rotate the video? Set by load()
- this.load = function(url, rotate) {
+ // These are the raw (unrotated) size of the poster image, which
+ // must have the same size as the video.
+ var videowidth, videoheight;
+
+ var playbackTime;
+ var capturedFrame;
+
+ this.load = function(video, posterimage, width, height, rotate) {
+ this.reset();
+ videourl = video;
+ posterurl = posterimage;
rotation = rotate || 0;
- player.mozAudioChannelType = 'content';
- player.src = url;
+ videowidth = width;
+ videoheight = height;
+ this.init();
+ setPlayerSize();
};
+ this.reset = function() {
+ hidePlayer();
+ hidePoster();
+ }
+
+ this.init = function() {
+ playbackTime = 0;
+ hidePlayer();
+ showPoster();
+ this.pause();
+ }
+
+ function hidePlayer() {
+ player.style.display = 'none';
+ player.removeAttribute('src');
+ player.load();
+ self.playerShowing = false;
+ }
+
+ function showPlayer() {
+ player.style.display = 'block';
+ player.src = videourl;
+ self.playerShowing = true;
+
+ // The only place we call showPlayer() is from the play() function.
+ // If play() has to show the player, call it again when we're ready to play.
+ player.oncanplay = function() {
+ player.oncanplay = null;
+ if (playbackTime !== 0) {
+ player.currentTime = playbackTime;
+ }
+ self.play();
+ }
+ }
+
+ function hidePoster() {
+ poster.style.display = 'none';
+ poster.removeAttribute('src');
+ if (capturedFrame) {
+ URL.revokeObjectURL(capturedFrame);
+ capturedFrame = null;
+ }
+ }
+
+ function showPoster() {
+ poster.style.display = 'block';
+ if (capturedFrame)
+ poster.src = capturedFrame;
+ else
+ poster.src = posterurl;
+ }
+
// Call this when the container size changes
this.setPlayerSize = setPlayerSize;
- // Set up everything for the initial paused state
this.pause = function pause() {
// Pause video playback
- player.pause();
+ if (self.playerShowing)
+ player.pause();
// Hide the pause button and slider
footer.classList.add('hidden');
@@ -75,12 +158,14 @@ function VideoPlayer(container) {
// Set up the playing state
this.play = function play() {
- // If we're at the end of the video, restart at the beginning.
- // This seems to happen automatically when an 'ended' event was fired.
- // But some media types don't generate the ended event and don't
- // automatically go back to the start.
- if (player.currentTime >= player.duration - 0.5)
- player.currentTime = 0;
+ if (!this.playerShowing) {
+ // If we're displaying the poster image, we have to switch
+ // to the player first. When the player is ready it wil call this
+ // function again.
+ hidePoster();
+ showPlayer();
+ return;
+ }
// Start playing the video
player.play();
@@ -102,8 +187,8 @@ function VideoPlayer(container) {
// Hook up the play button
playbutton.addEventListener('tap', function(e) {
- // If we're paused, go to the play state
- if (player.paused) {
+ // If we're not showing the player or are paused, go to the play state
+ if (!self.playerShowing || player.paused) {
self.play();
}
e.stopPropagation();
@@ -124,10 +209,9 @@ function VideoPlayer(container) {
}
});
- // Set the video size and duration when we get metadata
+ // Set the video duration when we get metadata
player.onloadedmetadata = function() {
durationText.textContent = formatTime(player.duration);
- setPlayerSize();
// start off in the paused state
self.pause();
};
@@ -150,6 +234,7 @@ function VideoPlayer(container) {
endedTimer = null;
}
self.pause();
+ self.init();
};
// Update the slider and elapsed time as the video plays
@@ -189,27 +274,67 @@ function VideoPlayer(container) {
}
}
+ // Pause and unload the video if we're hidden so that other apps
+ // can use the video decoder hardware.
+ window.addEventListener('mozvisibilitychange', visibilityChanged);
+
+ function visibilityChanged() {
+ if (document.mozHidden) {
+ // If we're just showing the poster image when we're hidden
+ // then we don't have to do anything special
+ if (!self.playerShowing)
+ return;
+
+ self.pause();
+
+ // If we're not at the beginning of the video, capture a
+ // temporary poster image to display when we come back
+ if (player.currentTime !== 0) {
+ playbackTime = player.currentTime;
+ captureCurrentFrame(function(blob) {
+ capturedFrame = URL.createObjectURL(blob);
+ hidePlayer();
+ showPoster();
+ });
+ }
+ else {
+ // Even if we don't capture a frame, hide the video
+ hidePlayer();
+ showPoster();
+ }
+ }
+ }
+
+ function captureCurrentFrame(callback) {
+ var canvas = document.createElement('canvas');
+ canvas.width = videowidth;
+ canvas.height = videoheight;
+ var context = canvas.getContext('2d');
+ context.drawImage(player, 0, 0);
+ canvas.toBlob(callback);
+ }
+
// Make the video fit the container
function setPlayerSize() {
var containerWidth = container.clientWidth;
var containerHeight = container.clientHeight;
// Don't do anything if we don't know our size.
// This could happen if we get a resize event before our metadata loads
- if (!player.videoWidth || !player.videoHeight)
+ if (!videowidth || !videoheight)
return;
var width, height; // The size the video will appear, after rotation
switch (rotation) {
case 0:
case 180:
- width = player.videoWidth;
- height = player.videoHeight;
+ width = videowidth;
+ height = videoheight;
break;
case 90:
case 270:
- width = player.videoHeight;
- height = player.videoWidth;
+ width = videoheight;
+ height = videowidth;
}
var xscale = containerWidth / width;
@@ -248,6 +373,7 @@ function VideoPlayer(container) {
transform += ' scale(' + scale + ')';
+ poster.style.transform = transform;
player.style.transform = transform;
}
@@ -303,11 +429,11 @@ function VideoPlayer(container) {
}
VideoPlayer.prototype.hide = function() {
- this.player.style.display = 'none';
+ // Call reset() to hide the poster and player
this.controls.style.display = 'none';
};
VideoPlayer.prototype.show = function() {
- this.player.style.display = 'block';
+ // Call init() to show the poster
this.controls.style.display = 'block';
};

0 comments on commit 4164424

Please sign in to comment.