Publish Vulpes Preview 15 sources for desktop, Android and Tundra

Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
This commit is contained in:
OverSu 2026-09-30 23:13:03 +02:00
commit b38dea0597
25901 changed files with 9982925 additions and 369 deletions

View file

@ -0,0 +1,15 @@
define(function(require, exports, module) {
'use strict';
module.exports = function(object) {
var key;
var fn;
for (key in object) {
fn = object[key];
if (typeof fn === 'function') {
object[key] = fn.bind(object);
}
}
};
});

30
gaia/apps/camera/js/lib/bind.js Executable file
View file

@ -0,0 +1,30 @@
define(function(require, exports, module) {
'use strict';
/**
* Exports
*/
exports = module.exports = bind;
/**
* addEventListener shorthand.
* @param {Element} el
* @param {String} name
* @param {Function} fn
*/
function bind(el, name, fn, capture) {
el.addEventListener(name, fn, capture || false);
}
/**
* removeEventListener shorthand.
* @param {Element} el
* @param {String} name
* @param {Function} fn
*/
exports.unbind = function(el, name, fn, capture) {
el.removeEventListener(name, fn, capture || false);
};
});

View file

@ -0,0 +1,11 @@
define(function(require, exports, module) {
'use strict';
module.exports = function(bytes) {
var bytesPerPixel = 3;
var avgJpegCompression = window.CONFIG_AVG_JPEG_COMPRESSION_RATIO || 8;
var uncompressedBytes = bytes * avgJpegCompression;
return Math.round(uncompressedBytes / bytesPerPixel);
};
});

View file

@ -0,0 +1,527 @@
/*
This module contains functions to deal with android camera coordinates.
Example use cases:
1. In the camera application we want to convert to screen coordinates
the top left corner of the faces detected by the camera so we can position a
DOM element to highlight the corresponding area.
2. We have to transform screen to android camera coordinates when
the user touches the screen to focus on a specific area.
In both cases we have to take into account the possible mismatch between the
screen pixels order and the order of the pixels sent by the driver.
ANDROID CAMERA COORDINATES
This documentation contain how android coordinates work and how the screen,
sensor and scene coordinates relate to one another.
The following is *always* true: the active rectangles of
the phone/screen and the sensor always -physically- line up.
It has to be thus, otherwise it would be impossible to
fill a portrait viewfinder with a portrait view, or a
landscape viewfinder with a landscape view.
Visually, this looks like:
phone/screen
+------+
| |
| |
| |
| |
| |
| |
| |
| |
+------+
sensor
+----+
| |
| |
| |
| |
+----+
detected face reported by the camera driver
-------------------------------------------
top
+-----+
| |
left | | right
| |
+-----+
bottom
The driver also reports width and height
If sensor orientation is 0, the sensor and the screen are scanned like this:
phone/screen
+------+
|aabbcc|
|aabbcc|
|ddeeff|
|ddeeff|
|gghhii|
|gghhii|
|jjkkll|
|jjkkll|
+------+
sensor
O---+ O = Origin of sensor coordinates (-1000, -1000)
|abc| X = (1000, 1000)
|def| sensor-to-screen conversion => none (x = x, y = y)
|ghi| face coordinate to convert => top, left
|jkl|
+---X
If there is an arrow pointing right:
o-->
...the sensor sees this arrow as such:
+----+
| |
|o-->|
| |
| |
+----+
...and if the sensor orientation is 0, the
arrow appears on the screen (magnified) like this:
+--------+
| |
| |
| |
|()====>>|
| |
| |
| |
| |
+--------+
If the sensor orientation is, e.g., 90-degrees--that is,
the "top" of the sensor corresponds to the "right" of the
screen--then without applying any rotation to the UI,
the arrow appears on the screen like this:
+--------+
| |
| |
| ^ |
| | |
| O |
| |
| |
| |
+--------+
The arrow is smaller, because the frames need to be
shrunk so that the entire height of the sensor image
fits into the width of the screen.
In this case, rotating the UI by 90-degrees clockwise,
and stretching it to fill the screen, gives:
+--------+
| |
| |
| |
|()====>>|
| |
| |
| |
| |
+--------+
This is because, in the 90-degree sensor
orientation case, the scanning is like this:
sensor
+---O O = Origin of sensor coordinates (-1000, -1000)
|iea| X = (1000, 1000)
|jfb| sensor-to-screen conversion => x = -y, y = x
|kgc| face coordinate to convert => bottom, left
|lhd|
X---+
And without compensating for this rotation,
the image on the screen would appear like this:
+------+
| |
| |
| abcd |
| efgh |
| ijkl |
| |
| |
| |
+------+
Thus sensor orientation has nothing to do with the physical
orientation of the sensor. Rather, it has to do with which
scan-line in the sensor is considered the "top".
Given all this, the coordinates of the "top-left" of
the sensor view, (-1000, -1000), are the coordinates of the
first pixel of the first scan-line. Similarly, the "bottom-right"
of the sensor is the last pixel of the last scan-line,
and has coordinates (1000, 1000).
----- 8< -----
As requested, the 180-degree case is:
phone/screen
+------+
|aabbcc|
|aabbcc|
|ddeeff|
|ddeeff|
|gghhii|
|gghhii|
|jjkkll|
|jjkkll|
+------+
sensor
X---+ O = Origin of sensor coordinates (-1000, -1000)
|jkl| X = (1000, 1000)
|ihg| sensor-to-screen conversion => x = -x, y = -y
|fed| face coordinate to convert => bottom, right
|cba|
+---O
And the 270-degree case is:
phone/screen
+------+
| |
| |
| abcd |
| efgh |
| ijkl |
| |
| |
| |
+------+
sensor
+---X O = Origin of sensor coordinates (-1000, -1000)
|dhl| X = (1000, 1000)
|cgk| sensor-to-screen conversion => x = y, y = -x
|bfj| face coordinate to convert => bottom, right
|aei|
O---+
*/
define(function(require, exports, module) {
/*jshint maxlen:false*/
'use strict';
/**
* It transforms camera coordinates to pixels
*
* @private
*/
function toPixels(x, y, viewportWidth, viewportHeight) {
// In camera coordinate system, (-1000, -1000) represents the
// top-left of the camera field of view, and (1000, 1000) represents
// the bottom-right.
// For convenience we assume that coordinates go from 0-2000
var cameraCoordinatesRange = 2000;
// How many pixels per camera unit?
var pixelsPerCameraUnitWidth = viewportWidth / cameraCoordinatesRange;
var pixelsPerCameraUnitHeight = viewportHeight / cameraCoordinatesRange;
// It makes the faces coordinates to go from 0-2000 for convenience
var xCameraCoordinates = x + 1000;
var yCameraCoordinates = y + 1000;
var xPixelCoordinates = xCameraCoordinates * pixelsPerCameraUnitWidth;
var yPixelCoordinates = yCameraCoordinates * pixelsPerCameraUnitHeight;
return {
x: Math.round(xPixelCoordinates),
y: Math.round(yPixelCoordinates)
};
}
/**
* It transforms pixels to camera coordinates
*
* @private
*/
function toCamera(x, y, viewportWidth, viewportHeight) {
// In camera coordinate system, (-1000, -1000) represents the
// top-left of the camera field of view, and (1000, 1000) represents
// the bottom-right.
var cameraCoordinatesRange = 2000;
// How many camera units per pixel?
var cameraUnitsPerPixelWidth = cameraCoordinatesRange / viewportWidth;
var cameraUnitsPerPixelHeight = cameraCoordinatesRange / viewportHeight;
return {
x: Math.round(x * cameraUnitsPerPixelWidth) - 1000,
y: Math.round(y * cameraUnitsPerPixelHeight) - 1000
};
}
/**
* It rotates camera coordinates given an angle 0, 90, 180, 270
*
* @private
*/
function rotatePoint(x, y, angle) {
// It clamps the angle to +/- 0-270
angle = (Math.round((angle % 360) / 90) % 4) * 90;
switch (angle) {
case 0:
return {
x: x,
y: y
};
case 90:
case -270:
return {
x: -y,
y: x
};
case 180:
case -180:
return {
x: -x,
y: -y
};
case 270:
case -90:
return {
x: y,
y: -x
};
default:
console.error('wrong angle value');
}
}
/**
* The sensor orientation is the orientation of the camera output
* (the order of the pixels sent from the sensor to the preview stream) with
* respect to the device screen in portrait orientation.
* This function matches the orientation of the sensor and screen coordinates
* We're interested on the top left and bottom-rignt corners of the
* area as observed through the lenses (scene coordinates).
* We take into account the orientation of the sensor to pick the correct
* corner that corresponds to the top most, left most, right most and
* bottom most coordinates in the physical/scene space as observed through the camera preview.
*
* @private
*/
function rotateArea(area, sensorOrientation) {
var topLeft = rotatePoint(area.left, area.top, sensorOrientation);
var bottomRight = rotatePoint(area.right, area.bottom, sensorOrientation);
// It picks the top left and bottom right corners on the scene / physical space
return {
top: Math.min(topLeft.y, bottomRight.y),
left: Math.min(topLeft.x, bottomRight.x),
bottom: Math.max(topLeft.y, bottomRight.y),
right: Math.max(topLeft.x, bottomRight.x),
width: Math.abs(topLeft.x - bottomRight.x),
height: Math.abs(topLeft.y - bottomRight.y)
};
}
/**
* It mirrors a given area in camera coordinates
* Use case: Display detected faces by the front camera.
*
* @private
*/
function mirrorAreaCamera(area) {
return {
top: area.top,
left: -area.right,
bottom: area.bottom,
right: -area.left,
width: area.width,
height: area.height
};
}
/**
* It mirrors a given area in camera coordinates
* Use case: Display detected faces by the front camera.
*
* @private
*/
function mirrorAreaPixels(area, viewportWidth) {
return {
top: area.top,
left: viewportWidth - area.right,
bottom: area.bottom,
right: viewportWidth - area.left,
width: area.width,
height: area.height
};
}
/**
* Given a distance in pixels and a screen size it returns
* the distance in camera units.
*
* @private
*/
function sizeToCamera(width, height, viewportWidth, viewportHeight) {
// In camera coordinate system, (-1000, -1000) represents the
// top-left of the camera field of view, and (1000, 1000) represents
// the bottom-right of the field of view.
// There are 2000 units in each axis of the camera coordinates
var cameraCoordinatesRange = 2000;
// How many camera units per pixel?
var cameraUnitsPerPixelWidth = cameraCoordinatesRange/ viewportWidth;
var cameraUnitsPerPixelHeight = cameraCoordinatesRange / viewportHeight;
return {
width: Math.round(width * cameraUnitsPerPixelWidth),
height: Math.round(height * cameraUnitsPerPixelHeight)
};
}
/**
* Given a distance in camera units and a screen size it returns
* the distance in pixels.
*
* @private
*/
function sizeToPixels(width, height, viewportWidth, viewportHeight) {
// In camera coordinate system, (-1000, -1000) represents the
// top-left of the camera field of view, and (1000, 1000) represents
// the bottom-right of the field of view.
// There are 2000 units in each axis of the camera coordinates
var cameraCoordinatesRange = 2000;
// How many pixels per camera coordinates unit?
var pixelsPerCameraUnitWidth = viewportWidth / cameraCoordinatesRange;
var pixelsPerCameraUnitHeight = viewportHeight / cameraCoordinatesRange;
return {
width: Math.round(width * pixelsPerCameraUnitWidth),
height: Math.round(height * pixelsPerCameraUnitHeight)
};
}
/**
* Given an area in screen camera units (-1000, 1000)
* and viewport dimensions it returns the same area
* in pixels
*
* @private
*/
function areaToPixels(area, viewportWidth, viewportHeight) {
// It converts the face from Android camera coordinates to pixels
var areaPixels = toPixels(
area.left, area.top, viewportWidth, viewportHeight);
// It converts the face size from Android camera units to pixels
var areaPixelSize = sizeToPixels(
area.width, area.height,
viewportWidth, viewportHeight);
var width = areaPixelSize.width;
var height = areaPixelSize.height;
return {
top: areaPixels.y,
left: areaPixels.x,
bottom: areaPixels.y + height,
right: areaPixels.x + width,
width: width,
height: height
};
}
/**
* Given an area in screen pixel coordinates
* and viewport dimensions it returns the same area
* in android camera coordinates (-1000, 1000)
*
* @public
*/
function areaToCamera(area, viewportWidth, viewportHeight) {
var topLeft = toCamera(
area.left, area.top, viewportWidth, viewportHeight);
// It converts the size from pixels to camera units
var areaCameraUnits = sizeToCamera(
area.width, area.height,
viewportWidth, viewportHeight);
var width = areaCameraUnits.width;
var height = areaCameraUnits.height;
var areaCamera = {
top: topLeft.y,
left: topLeft.x,
bottom: topLeft.y + height,
right: topLeft.x + width,
height: height,
width: width
};
return areaCamera;
}
/**
* Given face coordinates in pixels
* a viewport width and height it returns the face coordinates
* and area in camera units
*
* @public
*/
function faceToCamera(face, viewportWidth, viewportHeight, sensorOrientation, mirrored) {
if (mirrored) {
face = mirrorAreaPixels(face, viewportWidth);
}
face = areaToCamera(face, viewportWidth, viewportHeight);
// The orientation of the screen and the sensor might not match.
// We rotate the face so it matches screen coordinates orientation
if (sensorOrientation) {
face = rotateArea(face, -sensorOrientation);
}
return face;
}
/**
* Given face coordinates as provided by camera controller and
* a viewport width and height it returns the face coordinates, diameter
* and area in pixel units
*
* @public
*/
function faceToPixels(face, viewportWidth, viewportHeight, sensorOrientation, mirrored) {
// The orientation of the screen and the sensor might not match.
// We rotate the face so it matches screen coordinates orientation
if (sensorOrientation) {
face = rotateArea(face, sensorOrientation);
}
if (mirrored) {
face = mirrorAreaCamera(face);
}
return areaToPixels(face, viewportWidth, viewportHeight);
}
return {
// Face transformations
faceToCamera: faceToCamera,
faceToPixels: faceToPixels,
// Area transformations
private: {
// Area transformations
areaToCamera: areaToCamera,
areaToPixels: areaToPixels,
mirrorAreaCamera: mirrorAreaCamera,
mirrorAreaPixels: mirrorAreaPixels,
rotateArea: rotateArea,
// Size transformations
sizeToCamera: sizeToCamera,
sizeToPixels: sizeToPixels,
// Point transformations
toCamera: toCamera,
toPixels: toPixels,
rotatePoint: rotatePoint
}
};
});

View file

@ -0,0 +1,75 @@
define(function(require) {
/*jshint maxlen:false*/
'use strict';
var CameraUtils = {};
CameraUtils.scaleSizeToFitViewport = function(viewportSize, imageSize) {
var sw = viewportSize.width / imageSize.width,
sh = viewportSize.height / imageSize.height,
scale;
// Select the smaller scale to fit image completely within the viewport
scale = Math.min(sw, sh);
return {
width: imageSize.width * scale,
height: imageSize.height * scale
};
};
CameraUtils.scaleSizeToFillViewport = function(viewportSize, imageSize) {
var sw = viewportSize.width / imageSize.width,
sh = viewportSize.height / imageSize.height,
scale;
// Select the larger scale to fill and overflow viewport with image
scale = Math.max(sw, sh);
return {
width: imageSize.width * scale,
height: imageSize.height * scale
};
};
/**
* Get the maximum preview size (in terms of area) from a list of
* possible preview sizes.
*
* NOTE: If an `aspectRatio` value is provided, the search will be
* constrained to only accept preview sizes matching that aspect
* ratio.
*
* @param {Array} previewSizes
* @param {Number} aspectRatio
* @return {Object}
*/
CameraUtils.getMaximumPreviewSize = function(previewSizes, aspectRatio) {
// Use a very small tolerance because we want an exact match if we are
// constraining to only include specific aspect ratios.
const ASPECT_TOLERANCE = 0.001;
var maximumArea = 0;
var maximumPreviewSize = null;
previewSizes.forEach(function(previewSize) {
var area = previewSize.width * previewSize.height;
if (aspectRatio) {
var ratio = previewSize.width / previewSize.height;
if (Math.abs(ratio - aspectRatio) > ASPECT_TOLERANCE) {
return;
}
}
if (area > maximumArea) {
maximumArea = area;
maximumPreviewSize = previewSize;
}
});
return maximumPreviewSize;
};
return CameraUtils;
});

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,325 @@
define(function(require, exports, module) {
'use strict';
/**
* Module Dependencies
*/
var bindAll = require('lib/bind-all');
/**
* Exports
*/
module.exports = Focus;
/**
* Options:
*
* - {Boolean} `continousAutoFocus`
* - {Boolean} `touchFocus`
* - {Boolean} `faceDetection`
*/
function Focus(options) {
bindAll(this);
this.userPreferences = options || {};
this.detectedFaces = [];
}
Focus.prototype.configure = function(mozCamera, mode) {
var focusModes = mozCamera.capabilities.focusModes;
var focusMode;
this.mozCamera = mozCamera;
this.configureFocusModes(mode);
// Determines focus mode based on camera mode
// In case of C-AF enabled
if (this.continuousAutoFocus) {
if (mode === 'picture') {
focusMode = 'continuous-picture';
} else if (mode === 'video'){
focusMode = 'continuous-video';
}
}
// auto is the default focus mode
focusMode = focusMode || 'auto';
// If auto or passed mode is not supported we pick the
// first available in the hardware list
if (focusModes.indexOf(focusMode) === -1) {
focusMode = focusModes[0];
}
// If touch-to-focus is in progress we need to ensure
// the correct mode is restored when it is complete
this.suspendedMode = focusMode;
mozCamera.focusMode = focusMode;
this.reboot();
};
Focus.prototype.getMode = function() {
var mozCamera = this.mozCamera;
return this.suspendedMode || mozCamera.focusMode;
};
/**
* Configures focus modes based on user preferences
* and hardware availability
*/
Focus.prototype.configureFocusModes = function(mode) {
var userPreferences = this.userPreferences;
var continuousAutoFocusUserEnabled =
userPreferences.continuousAutoFocus !== false;
var touchFocusUserEnabled = userPreferences.touchFocus;
var touchFocusSupported = this.isTouchFocusSupported();
var faceDetectionUserEnabled = userPreferences.faceDetection;
var faceDetectionSupported = this.isFaceDetectionSupported();
this.continuousAutoFocus = continuousAutoFocusUserEnabled;
// User preferences override defaults
this.touchFocus = touchFocusUserEnabled && touchFocusSupported;
// Face detection only enabled on picture mode (disabled on video)
this.faceDetection =
faceDetectionUserEnabled && faceDetectionSupported && mode === 'picture';
this.mozCamera.addEventListener('focus', this.onAutoFocusStateChange);
};
Focus.prototype.startFaceDetection = function() {
if (!this.faceDetection) { return; }
this.mozCamera.addEventListener('facesdetected',
this.handleFaceDetectionEvent);
this.mozCamera.startFaceDetection();
};
Focus.prototype.stopFaceDetection = function() {
// Clear suspenstion timers
clearTimeout(this.faceDetectionSuspended);
clearTimeout(this.faceDetectionSuspensionTimer);
if (this.mozCamera.stopFaceDetection) {
this.mozCamera.removeEventListener('facesdetected',
this.handleFaceDetectionEvent);
this.mozCamera.stopFaceDetection();
}
this.clearFaceDetection();
};
Focus.prototype.handleFaceDetectionEvent = function(e) {
this.focusOnLargestFace(e.faces);
};
Focus.prototype.clearFaceDetection = function() {
this.focusOnLargestFace([]);
};
Focus.prototype.suspendFaceDetection = function(ms, delay) {
if (!this.faceDetection) {
return;
}
var self = this;
delay = delay || 0;
clearTimeout(this.faceDetectionSuspended);
clearTimeout(this.faceDetectionSuspensionTimer);
this.faceDetectionSuspensionTimer = setTimeout(suspendFaceDetection, delay);
function suspendFaceDetection() {
self.faceDetectionSuspended = setTimeout(clearTimer, ms);
}
function clearTimer() {
self.faceFocused = false;
self.faceDetectionSuspended = undefined;
}
};
Focus.prototype.stopContinuousFocus = function() {
var focusMode = this.mozCamera.focusMode;
// Clear suspension timers
clearTimeout(this.continuousModeTimer);
if (focusMode === 'continuous-picture' || focusMode === 'continuous-video') {
this.suspendedMode = this.mozCamera.focusMode;
this.mozCamera.focusMode = 'auto';
}
};
Focus.prototype.resumeContinuousFocus = function() {
this.mozCamera.focusMode = this.suspendedMode;
this.suspendedMode = null;
this.resetFocusAreas();
this.mozCamera.resumeContinuousFocus();
};
Focus.prototype.suspendContinuousFocus = function(ms) {
clearTimeout(this.continuousModeTimer);
this.stopContinuousFocus();
this.continuousModeTimer = setTimeout(this.resumeContinuousFocus, ms);
};
Focus.prototype.updateFocusState = function(state) {
// Only update if the state has changed, and only transition to focused
// or unfocused if we were previously focusing; this eliminates unfocused
// rings just before a focusing state change.
if (this.focusState !== state &&
(this.focusState === 'focusing' || state === 'focusing')) {
this.focusState = state;
this.onAutoFocusChanged(state);
}
};
Focus.prototype.onAutoFocusStateChange = function(e) {
var state = e.newState;
if (state === 'unfocused') {
state = 'fail';
}
this.updateFocusState(state);
};
Focus.prototype.onAutoFocusChanged = function(state) {
// NO OP by default
};
/**
* Callback invoked when faces are detected
* It's no op by default and it's meant to be overriden by
* the user of the module in order to be informed about detected faces
*/
Focus.prototype.onFacesDetected = function(faces) {
// NO OP by default
};
Focus.prototype.focusOnLargestFace = function(faces) {
if (this.faceDetectionSuspended) {
this.onFacesDetected([]);
return;
}
this.detectedFaces = faces;
this.onFacesDetected(this.detectedFaces);
};
/**
* Focus the camera, invoke the callback asynchronously when done.
*
* If we only have fixed focus, then we call the callback right away
* (but still asynchronously). Otherwise, we call autoFocus to focus
* the camera and call the callback when focus is complete. In C-AF mode
* this process should be fast. In manual AF mode, focusing takes about
* a second and causes a noticeable delay before the picture is taken.
*
* @param {Function} done
* @private
*/
Focus.prototype.focus = function(done) {
if (!this.mozCamera) { return; }
done = done || function() {};
var self = this;
this.suspendContinuousFocus(10000);
if (this.mozCamera.focusMode !== 'auto') {
done();
return;
}
//
// In either focus mode, we call autoFocus() to ensure that the user gets
// a sharp picture. The difference between the two modes is that if
// C-AF is on, it is likely that the camera is already focused, so the
// call to autoFocus() invokes its callback very quickly and we get much
// better response time.
//
// In either case, the callback is passed a boolean specifying whether
// focus was successful or not, and we display a green or red focus ring
// then call the done callback, which takes the picture and clears
// the focus ring.
//
this.updateFocusState('focusing');
this.mozCamera.autoFocus().then(onSuccess, onError);
// If focus fails with an error, we still need to signal the
// caller. Interruptions are a special case, but other errors
// can be treated the same as completing the operation but
// remaining unfocused.
function onError(err) {
self.focused = false;
if (err.name === 'NS_ERROR_IN_PROGRESS') {
done('interrupted');
} else {
done('error');
}
}
// This is fixed focus: there is nothing we can do here so we
// should just call the callback and take the photo. No focus
// happens so we don't display a focus ring.
function onSuccess(success) {
if (success) {
self.focused = true;
done('focused');
} else {
self.focused = false;
done('failed');
}
}
};
/**
* Resets focus regions
*/
Focus.prototype.resetFocusAreas = function() {
if (!this.touchFocus) {
return;
}
this.mozCamera.setFocusAreas([]);
this.mozCamera.setMeteringAreas([]);
};
Focus.prototype.pause = function() {
if (this.paused) { return; }
this.stopContinuousFocus();
this.stopFaceDetection();
this.paused = true;
delete this.focusState;
};
Focus.prototype.resume = function() {
if (!this.paused) { return; }
this.resumeContinuousFocus();
this.startFaceDetection();
this.paused = false;
};
Focus.prototype.reboot = function() {
this.pause();
this.resume();
};
/**
* Check if the hardware supports touch to focus
*/
Focus.prototype.isTouchFocusSupported = function() {
var maxFocusAreas = this.mozCamera.capabilities.maxFocusAreas;
return maxFocusAreas > 0;
};
/**
* Check if hardware supports face detection
*/
Focus.prototype.isFaceDetectionSupported = function() {
var cameraDetectsFaces = this.mozCamera.capabilities.maxDetectedFaces > 0;
var apiAvailable = !!this.mozCamera.startFaceDetection;
this.maxDetectedFaces = this.mozCamera.capabilities.maxDetectedFaces;
return cameraDetectsFaces && apiAvailable;
};
Focus.prototype.updateFocusArea = function(rect, done) {
done = done || function() {};
if (!this.touchFocus) {
done('touchToFocusNotAvailable');
return;
}
this.updateFocusState('focusing');
this.stopContinuousFocus();
this.suspendFaceDetection(10000);
this.mozCamera.setFocusAreas([rect]);
this.mozCamera.setMeteringAreas([rect]);
// Call auto focus to focus on focus area.
this.focus(done);
};
});

View file

@ -0,0 +1,35 @@
define(function(require, exports, module) {
'use strict';
var cropResizeRotate = require('cropResizeRotate');
/**
* 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.
*
* @param {Blob} imageBlob [description]
* @param {Object} metadata [description]
* @param {Object} thumbnailSize [description]
* @param {Function} done [description]
*/
module.exports = function(imageBlob, metadata, thumbnailSize, done) {
cropResizeRotate(imageBlob, null, thumbnailSize, null, metadata,
function(error, resizedBlob) {
// If we couldn't resize or rotate it, use the original
if (error) {
console.error('Error while resizing image: ' + error);
done(imageBlob);
return;
}
done(resizedBlob);
}
);
};
});

101
gaia/apps/camera/js/lib/dcf.js Executable file
View file

@ -0,0 +1,101 @@
define(function(require, exports, module) {
'use strict';
// This handles the logic pertaining to the naming of files according
// to the Design rule for Camera File System
// * http://en.wikipedia.org/wiki/Design_rule_for_Camera_File_system
/**
* Dependencies
*/
var asyncStorage = require('asyncStorage');
var debug = require('debug')('dcf');
var format = require('format');
/**
* Locals
*/
var dcfConfigLoaded = false;
var deferredArgs = null;
var defaultSeq = {file: 1, dir: 100};
var dcfConfig = {
key: 'dcf_key',
seq: null,
postFix: 'MZLLA',
prefix: {video: 'VID_', image: 'IMG_'},
ext: {video: '3gp', image: 'jpg'}
};
exports.init = function() {
debug('initializing');
exports.checkFileCounter(function() {
// We have a previous call to createDCFFilename that is waiting for
// a response, fire it again
if (deferredArgs) {
var args = deferredArgs;
exports.createDCFFilename(args.storage, args.type, args.callback);
deferredArgs = null;
}
debug('initialized');
});
};
exports.checkFileCounter = function(callback) {
asyncStorage.getItem(dcfConfig.key, function(value) {
dcfConfigLoaded = true;
dcfConfig.seq = value ? value : defaultSeq;
if (callback) {
callback();
}
});
};
exports.createDCFFilename = function(storage, type, callback) {
// We havent loaded the current counters from indexedDB yet, defer
// the call
if (!dcfConfigLoaded) {
deferredArgs = {storage: storage, type: type, callback: callback};
return;
}
var dir = 'DCIM/' + dcfConfig.seq.dir + dcfConfig.postFix + '/';
var filename = dcfConfig.prefix[type] +
format.padLeft(dcfConfig.seq.file, 4, '0') + '.' +
dcfConfig.ext[type];
var filepath = dir + filename;
// A file with this name may have been written by the user or
// our indexeddb sequence tracker was cleared, check we wont overwrite
// anything
var req = storage.get(filepath);
// A file existed, we bump the directory then try to generate a
// new filename
req.onsuccess = function() {
dcfConfig.seq.file = 1;
dcfConfig.seq.dir += 1;
asyncStorage.setItem(dcfConfig.key, dcfConfig.seq, function() {
exports.createDCFFilename(storage, type, callback);
});
};
// No file existed, we are good to go
req.onerror = function() {
if (dcfConfig.seq.file < 9999) {
dcfConfig.seq.file += 1;
} else {
dcfConfig.seq.file = 1;
dcfConfig.seq.dir += 1;
}
asyncStorage.setItem(dcfConfig.key, dcfConfig.seq, function() {
callback(filepath, filename, dir);
});
};
};
});

View file

@ -0,0 +1,19 @@
define(function(require, exports, module) {
'use strict';
module.exports = function(func, wait, immediate) {
var timeout;
return function() {
var context = this, args = arguments;
var later = function() {
timeout = null;
if (!immediate) { func.apply(context, args); }
};
var callNow = immediate && !timeout;
clearTimeout(timeout);
timeout = setTimeout(later, wait);
if (callNow) { func.apply(context, args); }
};
};
});

View file

@ -0,0 +1,81 @@
define(function(require, exports, module) {
'use strict';
/**
* Dependencies
*/
var getAspect = require('./get-aspect');
/**
* Returns a formatted list of picture
* sizes ready to be set as setting options.
*
* Options:
*
* - `maxPixelSize {Number}`
* - `exclude {Array}`
*
* @param {Array} sizes
* @param {Object} options
* @return {Array}
*/
module.exports = function(sizes, options) {
var maxPixelSize = options && options.maxPixelSize;
var exclude = options && options.exclude || {};
var include = options && options.include;
var formatted = [];
var hash = {};
exclude.aspects = exclude.aspects || [];
exclude.keys = exclude.keys || [];
sizes.forEach(function(size) {
var w = size.width;
var h = size.height;
var key = w + 'x' + h;
var pixelSize = w * h;
if (hash[key]) { return; }
size.aspect = getAspect(w, h);
// Don't include pictureSizes above the maxPixelSize limit
if (maxPixelSize && pixelSize > maxPixelSize) { return; }
if (include) {
if (include.keys && !~include.keys.indexOf(key)) { return; }
if (include.aspects && !~include.aspects.indexOf(size.aspect)) { return; }
}
if (exclude.keys.indexOf(key) > -1) { return; }
if (exclude.aspects.indexOf(size.aspect) > -1) { return; }
size.mp = getMP(w, h);
hash[key] = true;
formatted.push({
key: key,
pixelSize: pixelSize,
data: size
});
});
// Sort by pixel size
formatted.sort(function(a, b) { return b.pixelSize - a.pixelSize; });
return formatted;
};
/**
* Returns rounded mega-pixel value.
*
* @param {Number} w
* @param {Number} h
* @return {Number}
*/
function getMP(w, h) {
return Math.round((w * h) / 1000000);
}
});

View file

@ -0,0 +1,59 @@
define(function(require, exports, module) {
'use strict';
/**
* Dependencies
*/
var getAspect = require('./get-aspect');
/**
* Returns a formatted list of recorder
* profiles ready to be set as setting options.
*
* Options:
*
* - `exclude {Array}`
*
* @param {Object} profiles
* @param {Object} options
* @return {Array}
*/
module.exports = function(profiles, options) {
var exclude = options && options.exclude || [];
var items = [];
var hash = {};
for (var key in profiles) {
if (!profiles.hasOwnProperty(key)) { continue; } // Bug 1091820
var profile = profiles[key];
var video = profile.video;
var sizeKey = video.width + 'x' + video.height;
// guard against duplicate profiles
if (hash[sizeKey]) { continue; }
// Don't include profile if marked as excluded
if (exclude.indexOf(key) > -1) { continue; }
var pixelSize = video.width * video.height;
var aspect = getAspect(video.width, video.height);
hash[sizeKey] = key;
items.push({
key: key,
title: key + ' ' + sizeKey + ' ' + aspect,
pixelSize: pixelSize,
raw: profile
});
}
// Sort from largest to small but put the default/preferred profile first
items.sort(function(a, b) { return b.pixelSize - a.pixelSize; });
return items;
};
});

View file

@ -0,0 +1,35 @@
define(function(require, exports, module) {
'use strict';
var format = require('format');
/**
* Dependencies
*/
function digits(value) {
return format.padLeft(value, 2, '0');
}
/**
* Exports
*/
module.exports = function(ms) {
var totalSeconds = ms / 1000;
var seconds = Math.round(totalSeconds % 60);
var minutes = Math.floor(totalSeconds / 60);
var hours;
if (minutes < 60) {
return digits(minutes) + ':' + digits(seconds);
} else {
hours = Math.floor(minutes / 60);
minutes = Math.round(minutes % 60);
return hours + ':' + digits(minutes) + ':' + digits(seconds);
}
return '';
};
});

View file

@ -0,0 +1,68 @@
define(function(require, exports, module) {
'use strict';
/**
* Dependencies
*/
var debug = require('debug')('geolocation');
/**
* Exports
*/
module.exports = GeoLocation;
/**
* Interface to the
* geolocation API.
*
* @constructor
*/
function GeoLocation() {
this.watcher = null;
this.position = null;
this.setPosition = this.setPosition.bind(this);
this.watch = this.watch.bind(this);
}
/**
* Watches device location.
*
* @public
*/
GeoLocation.prototype.watch = function() {
if (!this.watcher) {
this.watcher = navigator.geolocation.watchPosition(this.setPosition);
debug('started watching');
}
};
/**
* Stops watching
* device location.
*
* @public
*/
GeoLocation.prototype.stopWatching = function() {
navigator.geolocation.clearWatch(this.watcher);
this.watcher = null;
debug('stopped watching');
};
/**
* Updates the stored
* position object.
*
* @private
*/
GeoLocation.prototype.setPosition = function(position) {
this.position = {
timestamp: position.timestamp,
altitude: position.coords.altitude,
latitude: position.coords.latitude,
longitude: position.coords.longitude
};
};
});

View file

@ -0,0 +1,20 @@
define(function(require, exports, module) {
'use strict';
/**
* Returns aspect ratio string.
*
* Makes use of Euclid's GCD algorithm,
* http://en.wikipedia.org/wiki/Euclidean_algorithm
*
* @param {Number} w
* @param {Number} h
* @return {String}
*/
module.exports = function(w, h) {
var gcd = function(a, b) { return (b === 0) ? a : gcd(b, a % b); };
var divisor = gcd(w, h);
return (w / divisor) + ':' + (h / divisor);
};
});

View file

@ -0,0 +1,8 @@
define(function(require, exports, module) {
'use strict';
module.exports = function(a, b) {
for (var key in b) { a[key] = b[key]; }
return a;
};
});

View file

@ -0,0 +1,81 @@
define(function(require, exports, module) {
'use strict';
var listener = require('device-orientation');
var classes = document.body.classList;
var current = 0;
listener.on('orientation', onOrientationChange);
listener.start();
function onOrientationChange(degrees) {
classes.remove('deg' + current);
classes.add('deg' + degrees);
current = degrees;
}
// Camera normally has its orientation locked to portrait mode.
// But we unlock orientation when displaying image and video previews.
// When orientation is unlocked, we call listener.stop().
// We calls call stop() when recording a video, and then restart
// when recording is done. If our app ever changes so that we can call
// unlock while the orientation listener is in the stopped state, then
// we would need to modify the lock() function so that it did not
// restart the listener. That is not needed now, however and is omitted.
function unlock() {
screen.mozUnlockOrientation();
listener.stop();
}
function lock() {
screen.mozLockOrientation('default');
listener.start();
}
function rationalize(sensorAngle, angle) {
if (typeof(angle) === 'undefined') {
angle = current;
}
// The result of this operation is an angle from 0..270 degrees,
// in steps of 90 degrees. Angles are rounded to the nearest
// magnitude, so 45 will be rounded to 90, and -45 will be rounded
// to -90 (not 0).
var r = angle + sensorAngle;
if (r >= 0) {
r += 45;
} else {
r -= 45;
}
r /= 90;
if (r >= 0) {
r = Math.floor(r);
} else {
r = Math.ceil(r);
}
r %= 4;
r *= 90;
if (r < 0) {
r += 360;
}
return r;
}
/**
* Exports
*/
module.exports = {
on: listener.on,
off: listener.off,
start: listener.start,
stop: listener.stop,
unlock: unlock,
lock: lock,
rationalize: rationalize,
get: function() {
return current;
}
};
});

View file

@ -0,0 +1,84 @@
define(function(require, exports, module) {
'use strict';
/**
* Dependencies
*/
var GestureDetector = require('GestureDetector');
/**
* Exports
*/
module.exports = addPanAndZoomHandlers;
/*
* This module adds pan-and-zoom capability to images displayed by
* shared/js/media/media_frame.js.
* It is used by preview-gallery.js and confirm.js
*/
function addPanAndZoomHandlers(frame, swipeCallback) {
// frame is the MediaFrame object. container is its DOM element.
var container = frame.container;
// Generate gesture events for the container
var gestureDetector = new GestureDetector(container);
gestureDetector.startDetecting();
// When the user touches the screen and moves their finger left or
// right, they might want to pan within a zoomed-in image, or they
// might want to swipe between multiple items in the camera preview
// gallery. We pass the amount of motion to the MediaFrame pan() method,
// and it returns the amount that cannot be used to pan the displayed
// item. We track this returned amount as how far left or right the
// image has been swiped, and pass the number to the swipeCallback.
var swipeAmount = 0;
// And handle them with these listeners
container.addEventListener('dbltap', handleDoubleTap);
container.addEventListener('transform', handleTransform);
container.addEventListener('pan', handlePan);
if (swipeCallback) {
container.addEventListener('swipe', handleSwipe);
}
function handleDoubleTap(e) {
var scale;
if (frame.fit.scale > frame.fit.baseScale) {
scale = frame.fit.baseScale / frame.fit.scale;
}
else {
scale = 2;
}
frame.zoom(scale, e.detail.clientX, e.detail.clientY, 200);
}
function handleTransform(e) {
frame.zoom(e.detail.relative.scale,
e.detail.midpoint.clientX, e.detail.midpoint.clientY);
}
function handlePan(e) {
var dx = e.detail.relative.dx;
var dy = e.detail.relative.dy;
if (swipeCallback) {
dx += swipeAmount;
swipeAmount = frame.pan(dx, dy);
swipeCallback(swipeAmount);
} else {
frame.pan(dx, dy);
}
}
function handleSwipe(e) {
if (swipeAmount !== 0) {
swipeCallback(swipeAmount, e.detail.vx);
swipeAmount = 0;
}
}
}
});

140
gaia/apps/camera/js/lib/pinch.js Executable file
View file

@ -0,0 +1,140 @@
define(function(require, exports, module) {
'use strict';
/**
* Dependencies
*/
var unbind = require('lib/bind').unbind;
var bindAll = require('lib/bind-all');
var bind = require('lib/bind');
var events = require('evt');
/**
* Mixin event emitter
*/
events(Pinch.prototype);
/**
* Exports
*/
module.exports = Pinch;
/**
* Initialize a new `Pinch` interface.
*
* @constructor
*/
function Pinch(el) {
bindAll(this);
this.attach(el);
}
Pinch.prototype.attach = function(el) {
this.el = el;
bind(this.el, 'touchstart', this.onTouchStart);
bind(window, 'touchmove', this.onTouchMove);
bind(window, 'touchend', this.onTouchEnd);
};
Pinch.prototype.detach = function() {
unbind(this.el, 'touchstart', this.onTouchStart);
unbind(window, 'touchmove', this.onTouchMove);
unbind(window, 'touchend', this.onTouchEnd);
this.el = null;
};
Pinch.prototype.enable = function() {
this.disabled = false;
};
Pinch.prototype.disable = function() {
this.disabled = true;
};
Pinch.prototype.onTouchStart = function(evt) {
if (evt.touches.length !== 2 || this.disabled) {
return;
}
this.lastTouchA = evt.touches[0];
this.lastTouchB = evt.touches[1];
this.isPinching = true;
this.emit('started');
};
Pinch.prototype.onTouchMove = function(evt) {
if (!this.isPinching || this.disabled) {
return;
}
var touchA = getNewTouchA(this, evt.touches);
var touchB = getNewTouchB(this, evt.touches);
var deltaPinch = getDeltaPinch(this, touchA, touchB);
this.emit('changed', deltaPinch);
this.lastTouchA = touchA;
this.lastTouchB = touchB;
};
Pinch.prototype.onTouchEnd = function(evt) {
if (!this.isPinching || this.disabled) {
return;
}
if (evt.touches.length < 2) {
this.isPinching = false;
this.emit('ended');
}
};
function getNewTouchA(pinch, touches) {
if (!pinch.lastTouchA) {
return null;
}
for (var i = 0, length = touches.length, touch; i < length; i++) {
touch = touches[i];
if (touch.identifier === pinch.lastTouchA.identifier) {
return touch;
}
}
return null;
}
function getNewTouchB(pinch, touches) {
if (!pinch.lastTouchB) {
return null;
}
for (var i = 0, length = touches.length, touch; i < length; i++) {
touch = touches[i];
if (touch.identifier === pinch.lastTouchB.identifier) {
return touch;
}
}
return null;
}
function getDeltaPinch(pinch, touchA, touchB) {
var lastTouchA = pinch.lastTouchA;
var lastTouchB = pinch.lastTouchB;
if (!touchA || !lastTouchA || !touchB || !lastTouchB) {
return 0;
}
var oldDistance = Math.sqrt(
Math.pow(lastTouchB.pageX - lastTouchA.pageX, 2) +
Math.pow(lastTouchB.pageY - lastTouchA.pageY, 2));
var newDistance = Math.sqrt(
Math.pow(touchB.pageX - touchA.pageX, 2) +
Math.pow(touchB.pageY - touchA.pageY, 2));
return newDistance - oldDistance;
}
});

View file

@ -0,0 +1,53 @@
define(function(require, exports, module) {
'use strict';
/**
* Module Dependencies
*/
var parseJpegMetadata = require('jpegMetaDataParser');
/**
* Exports
*/
module.exports = function(blob, done) {
parseJpegMetadata(blob, onJpegParsed);
function onJpegParsed(metadata) {
metadata.blob = blob;
if (!metadata.preview) {
done(metadata);
return;
}
// If we found an EXIF preview,
// and can determine its size, then
// we can display it instead of the
// big image and save memory and time.
var start = metadata.preview.start;
var end = metadata.preview.end;
var previewBlob = blob.slice(start, end, 'image/jpeg');
parseJpegMetadata(previewBlob, onSuccess, onError);
// If we parsed the preview image, add its
// dimensions to the metadata.preview
// object, and then let the MediaFrame
// object display the preview instead of
// the full-size image.
function onSuccess(previewMetadata) {
metadata.preview.width = previewMetadata.width;
metadata.preview.height = previewMetadata.height;
done(metadata);
}
// If we couldn't parse the preview image,
// just display full-size.
function onError() {
done(metadata);
}
}
};
});

View file

@ -0,0 +1,61 @@
define(function(require, exports, module) {
'use strict';
var Storage = require('lib/storage');
var cropResizeRotate = require('cropResizeRotate');
/**
* Exports
*/
module.exports = function(options, done) {
var blob = options.blob;
var outputSize = options.width && options.height ?
{
width: options.width,
height: options.height
} : options.size || null;
cropResizeRotate(blob, null, outputSize, 'image/jpeg+exif',
function(error, resizedBlob) {
// If we couldn't resize or rotate it, use the original
if (error) {
console.error('Error while resizing image: ' + error);
done(blob);
return;
}
// We need to send a file-backed blob as the result of a pick activity
// (see bug 975599) so we'll overwrite the old blob with the new one.
// This means that the image stored will actually match the one passed
// to the app that initiated the pick activity. We delete the old file,
// then save the new blob with the same name. Then we read the file and
// pass that to the callback.
if (resizedBlob === blob) {
done(blob);
return;
}
var storage = new Storage();
// We first delete the full resolution picture
storage.deletePicture(blob.name, addPicture);
// We save the scaled down image
function addPicture(error) {
if (error) {
done(blob);
return;
}
storage.addPicture(resizedBlob, {
filepath: blob.name
}, onSavedPicture);
}
function onSavedPicture(error, filepath, absolutePath, fileBlob) {
done(fileBlob);
}
});
};
});

View file

@ -0,0 +1,157 @@
define(function(require, exports, module) {
'use strict';
/**
* Dependencies
*/
var debug = require('debug')('setting-alias');
/**
* Locals
*/
var forwardMethods = [
'filterOptions',
'resetOptions',
'supported',
'selected',
'select',
'next',
'get',
'set'
];
/**
* Exports
*/
module.exports = SettingAlias;
/**
* Initialize a new `SettingsAlias`
*
* SettingsAlias's are kinda confusing,
* but they simplify our app code massively
* in the long run.
*
* A `SettingAlias` has the same API as a `Setting`
* but behind the scenes provides a single interface
* to several `Setting`s. Depending on the state
* of the app, the `SettingAlias` will point to
* a different settings instance.
*
* Example:
*
* var flashModesPicture = new Setting({ key: 'flashModesPicture' });
* var flashModesVideo = new Setting({ key: 'flashModesVideo' });
* var mode = 'picture';
*
* var flashModes = new SettingAlias({
* key: 'flashModes',
* settings: {
* 'picture': flashModesPicture,
* 'video': flashModesVideo
* },
* get: function() {
* return this.settings[mode];
* }
* });
*
* flashModes.get('key'); //=> 'flashModesPicture'
* mode = 'video';
* flashModes.get('key'); //=> 'flashModesVideo'
*
* This means our app can simply call API on
* `flashModes` setting-alias and not worry
* about which 'mode' the app is in.
*
* Options:
*
* - `key` The name of the setting-alias
* - `settings` Hash of possible `Setting`s
* - `get` Method that returns the current `Setting`
*
* @param {Object} options
*/
function SettingAlias(options) {
debug('initialize');
this.key = options.key;
this.settings = options.settings;
this.current = options.get.bind(this);
forwardMethods.forEach(this.forward, this);
debug('initialized');
}
/**
* Iterates over each setting.
*
* @param {Function} fn
* @private
*/
SettingAlias.prototype.each = function(fn) {
for (var key in this.settings) {
fn(this.settings[key]);
}
};
/**
* States whether the passed key
* is the currently active setting.
*
* @param {String} key
* @return {Boolean}
*/
SettingAlias.prototype.is = function(key) {
return this.get('key') === key;
};
/**
* Attaches a method that forwards
* the call onto the current
* matching setting.
*
* @param {String} method
*/
SettingAlias.prototype.forward = function(method) {
this[method] = function() {
var setting = this.current();
return setting[method].apply(setting, arguments);
};
};
/**
* Add an event callback.
*
* The callback is wrapped in
* a function that first checks that
* the setting is the active setting
* before calling the original callback.
*
* @param {String} name
* @param {Function} fn
*/
SettingAlias.prototype.on = function(name, fn) {
var alias = this;
var wrapped = function() {
if (!alias.is(this.key)) { return; }
fn.apply(this, arguments);
};
this.each(function(setting) { setting.on(name, wrapped); });
fn._settingAliasCallback = wrapped;
};
/**
* Remove an event callback.
*
* @param {String} name
* @param {Function} fn
*/
SettingAlias.prototype.off = function(name, fn) {
var wrapped = fn._settingAliasCallback;
this.each(function(setting) { setting.off(name, wrapped); });
delete fn._settingAliasCallback;
};
});

View file

@ -0,0 +1,251 @@
define(function(require, exports, module) {
'use strict';
/**
* Dependencies
*/
var debug = require('debug')('setting');
var model = require('model');
/**
* Exports
*/
module.exports = Setting;
/**
* Mixin `Model` methods
*/
model(Setting.prototype);
/**
* Initialize a new `Setting`.
*
* @param {Object} data
*/
function Setting(data) {
this.key = data.key;
this.storage = data.storage || localStorage;
this.reset(data, { silent: true });
this.select = this.select.bind(this);
this.next = this.next.bind(this);
this.configure(data);
if (data.persistent) { this.on('change:selected', this.save); }
}
Setting.prototype.configure = function(data) {
var optionsDefined = !!(data.options && data.options.length);
this.options = { defined: optionsDefined };
this.resetOptions(data.options);
};
Setting.prototype.resetOptions = function(list) {
list = list || [];
var hash = this.optionsToHash(list);
this.options.all = hash;
this.options.available = hash;
this.set('options', list, { silent: true });
this.updateSelected({ silent: true });
};
Setting.prototype.filterOptions = function(keys, options) {
var available = this.options.available = {};
var hash = this.options.all;
var filtered = [];
(keys || []).forEach(function(key) {
var option = hash[key];
if (option !== undefined) {
filtered.push(option);
available[key] = option;
}
});
this.sortByIndex(filtered);
this.set('options', filtered, options);
this.updateSelected({ silent: true });
};
/**
* Convert this Setting's `options` array as defined
* in config/app.js into an object as key/value pairs.
* This allows us to look up an option by its `key`.
*
* e.g.: [ { key: 'a', title: 'A' }, ... ] ->
* { a: { key: 'a', title: 'A' }, ... }
*
* We use this hash to validate
* incoming options and ot mixin
* config data with dynamic options.
*
* @param {Array} options
* @return {undefined|Object}
*/
Setting.prototype.optionsToHash = function(options) {
var hash = {};
options.forEach(function(option, index) {
option.index = index;
hash[option.key] = option;
});
return hash;
};
/**
* Get the selected option,
* or just a particular key
* if given.
*
* @param {String} key
* @return {Object|*}
*/
Setting.prototype.selected = function(key) {
var hash = this.options.available;
var option = hash[this.get('selected')];
return key ? option && option[key] : option;
};
/**
* Select an option.
*
* Accepts an string key to select,
* or an integer index relating to
* the current options list.
*
* Options:
*
* - {Boolean} silent
*
* @param {String|Number} key
* @param {Object} opts Model#set() options
*/
Setting.prototype.select = function(key, options) {
var isIndex = typeof key === 'number';
var list = this.get('options');
var available = this.options.available;
var selected = isIndex ? list[key] : available[key];
// If there are no options, exit
if (!list.length) { return; }
// If an option was not found,
// default to selecting the first.
if (!selected) { return this.select(0, options); }
// Store the new choice
this.set('selected', selected.key, options);
};
/**
* Sorts the current options list
* by their originally defined
* index in the config JSON.
*
* @private
*/
Setting.prototype.sortByIndex = function(list) {
return list.sort(function(a, b) { return a.index - b.index; });
};
/**
* Silently updates the `selected`
* property of the Setting.
*
* If no selected key is present
* we attempt to select the last
* fetched persited selection.
*
* @private
*/
Setting.prototype.updateSelected = function(options) {
this.select(this.get('selected') || this.fetched, options);
};
/**
* Set the `selected` option to
* the next option in the list.
*
* First option is chosen if
* there is no next option.
*
* @public
*/
Setting.prototype.next = function() {
var options = this.get('options');
var selected = this.selected();
var index = options.indexOf(selected);
var newIndex = (index + 1) % options.length;
this.select(newIndex);
debug('set \'%s\' to index: %s', this.key, newIndex);
};
/**
* Persists the current selection
* to storage for retreval in the
* next session.
*
* We're using localStorage as performance
* vastly outweighed indexedBD (asyncStorage)
* by 1ms/500ms on Hamachi device.
*
* @public
*/
Setting.prototype.save = function() {
var selected = this.get('selected');
debug('saving key: %s, selected: %s', this.key, selected);
this.storage.setItem('setting:' + this.key, selected);
debug('saved key: %s', selected);
};
/**
* Fetches the persisted selection
* from storage, updating the
* `selected` key.
*
* We're using localStorage, as performance
* vastly outweighed indexedBD (asyncStorage)
* by 1ms/500ms on Hamachi device.
*
* Leaving in the `done` callback in-case
* storage goes async again in future.
*
* @param {Function} done
* @public
*/
Setting.prototype.fetch = function() {
if (!this.get('persistent')) { return; }
debug('fetch value key: %s', this.key);
this.fetched = this.storage.getItem('setting:' + this.key);
debug('fetched %s value: %s', this.key, this.fetched);
if (this.fetched) { this.select(this.fetched, { silent: true }); }
};
/**
* States whether this setting
* is currently supported.
*
* 'Supported' means, it's not been
* disabled, and there are options
* to be chosen from.
*
* @return {Boolean}
*/
Setting.prototype.supported = function() {
return this.enabled() && !!this.get('options').length;
};
/**
* Check if this setting is not
* marked as disabled.
*
* @return {Boolean}
*/
Setting.prototype.enabled = function() {
return !this.get('disabled');
};
});

View file

@ -0,0 +1,105 @@
define(function(require, exports, module) {
'use strict';
/**
* Dependencies
*/
var SettingAlias = require('./setting-alias');
var debug = require('debug')('settings');
var Setting = require('./setting');
/**
* Exports
*/
module.exports = Settings;
/**
* Initialize a new 'Setting'
*
* @param {Object} items
*/
function Settings(items) {
this.ids = {};
this.items = [];
this.aliases = {};
this.SettingAlias = SettingAlias; // Test hook
this.dontSave = this.dontSave.bind(this);
this.addEach(items);
}
/**
* Add several Settings in one go.
*
* @param {Object} items
*/
Settings.prototype.addEach = function(items) {
if (!items) { return; }
var item;
var key;
for (key in items) {
item = items[key];
item.key = item.key || key;
this.add(items[key]);
}
};
/**
* Add a new Setting to the settings collection.
*
* @param {Object} data
*/
Settings.prototype.add = function(data) {
var setting = new Setting(data);
this.items.push(setting);
this.ids[setting.key] = this[setting.key] = setting;
debug('added setting: %s', setting.key);
};
/**
* Fetch all the Settings previous
* state from storage.
*
* @public
*/
Settings.prototype.fetch = function() {
this.items.forEach(function(setting) { setting.fetch(); });
};
/**
* Prevent all settings from saving
* when they are changed.
*
* This is used in activity sessions
* whereby we don't want any settings
* changes to persist to future camera
* sessions.
*
* @public
*/
Settings.prototype.dontSave = function() {
this.items.forEach(function(setting) {
setting.off('change:selected', setting.save);
});
};
/**
* Add a SettingAlias to the settings.
* (see lib/setting-alias.js for more info)
*
* @param {Object} options
* @public
*/
Settings.prototype.alias = function(options) {
var alias = new this.SettingAlias(options);
this.aliases[options.key] = alias;
this[options.key] = alias;
};
Settings.prototype.removeAlias = function(key) {
// TODO: Implement
};
});

162
gaia/apps/camera/js/lib/sounds.js Executable file
View file

@ -0,0 +1,162 @@
define(function(require, exports, module) {
'use strict';
/**
* Exports
*/
module.exports = Sounds;
/**
* Initialize a new `Sounds` interface.
*
* @constructor
*/
function Sounds(list) {
this.items = {};
(list || []).forEach(this.add, this);
}
/**
* Add a new sound.
*
* Checks if this sound
* is enabled, and sets
* up an observer.
*
* @param {Object} data
*
*/
Sounds.prototype.add = function(data) {
var self = this;
var sound = {
name: data.name,
url: data.url,
setting: data.setting,
enabled: false
};
this.items[data.name] = sound;
this.isEnabled(sound, function(value) {
self.setEnabled(sound, value);
self.observeSetting(sound);
});
};
/**
* Check if a sound is
* enabled inside mozSettings.
*
* This is wrapped in a setTimeout
* to prevent expensive mozSettings
* API from blocking other operations.
*
* @param {Object} sound
* @param {Function} done
*/
Sounds.prototype.isEnabled = function(sound, done) {
setTimeout(function() {
var mozSettings = navigator.mozSettings;
var key = sound.setting;
// Requires navigator.mozSettings
if (!mozSettings) {
return;
}
mozSettings
.createLock()
.get(key)
.onsuccess = onSuccess;
function onSuccess(e) {
var result = e.target.result[key];
done(result);
}
});
};
/**
* Observe mozSettings for changes
* on the given settings key.
*
* @param {Object} sound
*
*/
Sounds.prototype.observeSetting = function(sound) {
var mozSettings = navigator.mozSettings;
var key = sound.setting;
var self = this;
if (mozSettings) {
mozSettings.addObserver(key, function(e) {
self.setEnabled(sound, e.settingValue);
});
}
};
/**
* Set a sounds `enabled` key.
*
* @param {Object} sound
* @param {Boolean} value
*
*/
Sounds.prototype.setEnabled = function(sound, value) {
sound.enabled = value;
};
/**
* Play a sound by name.
*
* @param {String} name
* @public
*/
Sounds.prototype.play = function(name) {
var sound = this.items[name];
if (!sound.audio) {
sound.audio = this.createAudio(sound.url);
}
this.playSound(sound);
};
/**
* Return a function that plays
* the given sound when called.
*
* @param {String} name
* @return {Funciton}
* @public
*/
Sounds.prototype.player = function(name) {
return (function() { this.play(name); }).bind(this);
};
/**
* Play a sound.
*
* @param {Object} sound
*
*/
Sounds.prototype.playSound = function(sound) {
if (sound.enabled) {
// cloneNode() lets us play overlapping sounds and is needed when the
// user takes pictures very rapidly.
// See http://robert.ocallahan.org/2011/11/latency-of-html5-sounds.html
sound.audio.cloneNode(true).play();
}
};
/**
* Create an audio element.
*
* @param {String} url
* @return {HTMLAudioElement}
*/
Sounds.prototype.createAudio = function(url) {
var audio = new Audio(url);
audio.mozAudioChannelType = 'notification';
return audio;
};
});

View file

@ -0,0 +1,410 @@
define(function(require, exports, module) {
'use strict';
/**
* Dependencies
*/
var debug = require('debug')('storage');
var bindAll = require('lib/bind-all');
var events = require('evt');
var storageSingleton;
/**
* Expose `Storage`
*/
module.exports = Storage;
/**
* Mixin event emitter
*/
events(Storage.prototype);
/**
* Initialize a new `Storage`.
*
* @param {Object} options
*/
function Storage(options) {
if (storageSingleton) {
return storageSingleton;
}
storageSingleton = this;
bindAll(this);
this.maxFileSize = 0;
options = options || {};
this.dcf = options.dcf;
if (this.dcf) {
this.dcf.init();
}
this.require = options.require || require;
navigator.mozSettings.addObserver(
'device.storage.writable.name',
this.onStorageVolumeChanged);
this.configure();
debug('initialized');
}
Storage.prototype.createFilename = function(storage, type, done) {
var self = this;
this.require(['asyncStorage', 'lib/dcf'], function(as, dcf) {
if (!self.dcf) {
self.dcf = dcf;
self.dcf.init();
}
self.dcf.createDCFFilename(storage, type, done);
});
};
/**
* Save the image Blob to DeviceStorage
* then lookup the File reference and
* return that in the callback as well
* as the resulting paths.
*
* You always want to forget about the
* Blob you told us about and use the
* File instead since otherwise you
* are wasting precious memory.
*
* @param {Object} [options]
* @param {String} options.filepath
* The path to save the image to.
*/
Storage.prototype.addPicture = function(blob, options, done) {
if (typeof options === 'function') {
done = options;
options = {};
}
done = done || function() {};
var filepath = options && options.filepath;
var self = this;
debug('add picture', filepath);
// Create a filepath if
// one hasn't been given.
if (!filepath) {
debug('creating filename');
this.createFilename(this.picture, 'image', onCreated);
} else {
onCreated(filepath);
}
function onCreated(filepath) {
var req = self.picture.addNamed(blob, filepath);
req.onerror = function() { self.emit('error'); };
req.onsuccess = function(e) {
debug('image stored', filepath);
var absolutePath = e.target.result;
// `addNamed` does not give us a File
// handle so we need to get() it again.
refetchFile(filepath, absolutePath);
};
}
function refetchFile(filepath, absolutePath) {
var req = self.picture.get(filepath);
req.onerror = function() {
self.emit('error');
done('Error adding picture to storage');
};
req.onsuccess = function(e) {
debug('image file blob handle retrieved');
var fileBlob = e.target.result;
done(null, filepath, absolutePath, fileBlob);
};
}
};
/**
* Create a new video filepath.
*
* The CameraControl API will not
* automatically create directories
* for the new file if they do not
* exist.
*
* So we write a dummy file to the
* same directory via DeviceStorage
* to ensure that the directory exists
* before attempting to record to this
* filepath.
*
* If the request errors it can mean that
* there is a file that already exists
* in that name. If so, we attempt to delete
* it and try to create the filepath again.
*
* @param {Function} done
* @public
*/
Storage.prototype.createVideoFilepath = function(done) {
var videoStorage = this.video;
var self = this;
this.createFilename(this.video, 'video', function(filepath) {
var dummyFilepath = getDir(filepath) + '.tmp.3gp';
var blob = new Blob([''], { type: 'video/3gpp' });
var req = videoStorage.addNamed(blob, dummyFilepath);
req.onerror = function(e) {
debug('Failed to add ' + filepath + ' to DeviceStorage', e);
var req = videoStorage.delete(dummyFilepath);
req.onerror = function() { done('Error creating video file path'); };
req.onsuccess = function() { self.createVideoFilepath(done); };
};
req.onsuccess = function(e) {
videoStorage.delete(e.target.result);
done(null, filepath);
};
});
};
Storage.prototype.onStorageChange = function(e) {
debug('state change: %s', e.reason);
var value = e.reason;
switch (value) {
case 'deleted':
// Emit an `itemdeleted` event to
// allow parts of the UI to update.
var filepath = this.checkFilepath(e.path);
this.emit('itemdeleted', { path: filepath });
break;
case 'available':
case 'shared':
case 'unavailable':
this.setState(value);
break;
}
// Check storage
// has spare capacity
this.check();
};
Storage.prototype.configure = function(storageVolumeName) {
var i;
var videosStorages;
var picturesStorages;
// If we had a previous ds for pictures, let's remove the observer
// we had set as well before fetching new ds.
if (this.picture) {
this.picture.removeEventListener('change', this.onStorageChange);
}
if (!storageVolumeName) {
this.video = navigator.getDeviceStorage('videos');
this.picture = navigator.getDeviceStorage('pictures');
} else { // We select the volumes with the passed name
videosStorages = navigator.getDeviceStorages('videos');
this.video = videosStorages[0];
for (i = 0; i < videosStorages.length; ++i) {
if (videosStorages[i].storageName === storageVolumeName) {
this.video = videosStorages[i];
break;
}
}
picturesStorages = navigator.getDeviceStorages('pictures');
this.picture = picturesStorages[0];
for (i = 0; i < picturesStorages.length; ++i) {
if (picturesStorages[i].storageName === storageVolumeName) {
this.picture = picturesStorages[i];
break;
}
}
}
// Shouldn't happen?
if (!this.picture) {
this.setState('unavailable');
return;
}
this.picture.addEventListener('change', this.onStorageChange);
this.emit('volumechanged',{
video: this.video,
picture: this.picture
});
};
Storage.prototype.onStorageVolumeChanged = function(setting) {
debug('default storage volume change: %s', setting.settingValue);
this.configure(setting.settingValue);
};
Storage.prototype.checkFilepath = function(filepath) {
var startString = filepath.indexOf('DCIM/');
if (startString < -1) { return; }
else if (startString > 0) {
filepath = filepath.substr(startString);
}
// Check whether filepath is a video poster image or not. If filepath
// contains 'VID' and ends with '.jpg', consider it a video poster
// image and get the video filepath by changing '.jpg' to '.3gp'
if (filepath.indexOf('VID') != -1 &&
filepath.lastIndexOf('.jpg') === filepath.length - 4) {
filepath = filepath.replace('.jpg', '.3gp');
}
return filepath;
};
Storage.prototype.setState = function(value) {
this.state = value;
debug('set state: %s', value);
this.emit('changed', value);
};
Storage.prototype.setMaxFileSize = function(maxFileSize) {
this.maxFileSize = maxFileSize;
this.check();
debug('max file size set: %d', maxFileSize);
};
/**
* Run a full storage check.
*
* @param {Function} done
* @public
*/
Storage.prototype.check = function(done) {
debug('check');
var self = this;
done = done || function() {};
this.getState(function(result) {
self.setState(result);
if (!self.available()) {
onComplete('unhealthy');
return;
}
// check file counter in dcf if it already exist.
if (self.dcf) {
self.dcf.checkFileCounter();
}
self.isSpace(function(result) {
if (!result) { self.setState('nospace'); }
onComplete('healthy');
});
});
function onComplete(state) {
self.emit('checked', state);
}
};
/**
* Checks if there is enough space to
* accomdate the current `maxFileSize`.
*
* @param {Function} done
*/
Storage.prototype.isSpace = function(done) {
var maxFileSize = this.maxFileSize;
this.picture
.freeSpace()
.onsuccess = function(e) {
var freeSpace = e.target.result;
var result = freeSpace > maxFileSize;
debug('is space: %s', result, freeSpace, maxFileSize);
done(result);
};
};
/**
* Get current storage state.
*
* @param {Function} done
*/
Storage.prototype.getState = function(done) {
if (!this.picture) {
setTimeout(function() {
done('unavailable');
});
return;
}
this.picture
.available()
.onsuccess = function(e) {
done(e.target.result);
};
};
/**
* States if sotrage is available.
*
* @return {Boolean}
*/
Storage.prototype.available = function() {
return this.state === 'available';
};
/**
* Delete a picture.
*
* @param {String} filepath
*/
Storage.prototype.deletePicture = function(filepath, done) {
var req = this.picture.delete(filepath);
req.onerror = function(e) {
var message = 'Failed to delete ' + filepath +
' from DeviceStorage:' + e.target.error;
console.warn(message);
done(message);
};
if (done) {
req.onsuccess = function() {
done(null);
};
}
};
/**
* Delete a video and accompanying
* poster image.
*
* @param {String} filepath
*/
Storage.prototype.deleteVideo = function(filepath) {
var poster = filepath.replace('.3gp', '.jpg');
this.video.delete(filepath).onerror = function(e) {
console.warn(
'Failed to delete', filepath,
'from DeviceStorage:', e.target.error);
};
this.picture.delete(poster).onerror = function(e) {
console.warn(
'Failed to delete poster image', poster,
'for video', filepath,
'from DeviceStorage:', e.target.error);
};
};
/**
* Get the directory from a filepath.
*
* @param {String} filepath
* @return {String}
*/
function getDir(filepath) {
var index = filepath.lastIndexOf('/') + 1;
return index ? filepath.substring(0, index) : '';
}
});

View file

@ -0,0 +1,27 @@
define(function(require) {
'use strict';
var StringUtils = {};
StringUtils.toCamelCase = function(str) {
// Convert SNAKE_CASE strings to hyphenated
str = str.toLowerCase().replace(/_/g, '-');
return str.replace(/-([a-z])/g, function(g) {
return g[1].toUpperCase();
});
};
StringUtils.toHyphenate = function(str) {
return str.replace(/([a-z][A-Z])/g, function(g) {
return g[0] + '-' + g[1].toLowerCase();
});
};
StringUtils.lastPathComponent = function(path) {
var pathComponents = path.split('/');
return pathComponents[pathComponents.length - 1];
};
return StringUtils;
});