Bug 1062387 - Part 2. Implement DOM and JavaScript facing components of JS camera driver. r=mikeh r=bz
authorAndrew Osmond <aosmond@gmail.com>
Sun, 01 Mar 2015 13:48:37 -0500
changeset 232833 b7eca7da161d357551399fdb89c145142778c398
parent 232832 f15a86a87f9e773a83113cb851f40b35b2605147
child 232834 1072484a6e5f61df1a6930d8c5df305dc9ab0065
push id56670
push userkwierso@gmail.com
push dateTue, 10 Mar 2015 23:12:10 +0000
treeherdermozilla-inbound@fd8e079d6335 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmikeh, bz
bugs1062387
milestone39.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1062387 - Part 2. Implement DOM and JavaScript facing components of JS camera driver. r=mikeh r=bz
b2g/installer/package-manifest.in
dom/camera/CameraTestHardware.js
dom/camera/CameraTestHardware.manifest
dom/camera/DOMCameraDetectedFace.cpp
dom/camera/DOMCameraDetectedFace.h
dom/camera/DOMCameraManager.cpp
dom/camera/moz.build
dom/camera/nsICameraTestHardware.idl
dom/camera/test/camera_common.js
dom/webidl/CameraControl.webidl
--- a/b2g/installer/package-manifest.in
+++ b/b2g/installer/package-manifest.in
@@ -181,16 +181,19 @@
 #ifdef MOZ_B2G_RIL
 @RESPATH@/components/dom_icc.xpt
 @RESPATH@/components/dom_wappush.xpt
 @RESPATH@/components/dom_mobileconnection.xpt
 #endif
 #ifdef MOZ_B2G_BT
 @RESPATH@/components/dom_bluetooth.xpt
 #endif
+#ifdef MOZ_B2G_CAMERA
+@BINPATH@/components/dom_camera.xpt
+#endif
 @RESPATH@/components/dom_canvas.xpt
 @RESPATH@/components/dom_contacts.xpt
 @RESPATH@/components/dom_alarm.xpt
 @RESPATH@/components/dom_core.xpt
 @RESPATH@/components/dom_css.xpt
 @RESPATH@/components/dom_devicestorage.xpt
 @RESPATH@/components/dom_events.xpt
 @RESPATH@/components/dom_geolocation.xpt
@@ -452,16 +455,22 @@
 @RESPATH@/components/NetworkStatsServiceProxy.js
 @RESPATH@/components/NetworkStatsServiceProxy.manifest
 @RESPATH@/components/TetheringService.js
 @RESPATH@/components/TetheringService.manifest
 @RESPATH@/components/WifiWorker.js
 @RESPATH@/components/WifiWorker.manifest
 #endif // MOZ_WIDGET_GONK
 
+; Camera
+#ifdef MOZ_B2G_CAMERA
+@BINPATH@/components/CameraTestHardware.js
+@BINPATH@/components/CameraTestHardware.manifest
+#endif // MOZ_B2G_CAMERA
+
 ; Tethering
 #ifdef MOZ_WIDGET_GONK
 @RESPATH@/components/TetheringManager.js
 @RESPATH@/components/TetheringManager.manifest
 #endif
 
 ; ResourceStats
 #ifdef MOZ_WIDGET_GONK
new file mode 100644
--- /dev/null
+++ b/dom/camera/CameraTestHardware.js
@@ -0,0 +1,214 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/DOMRequestHelper.jsm");
+
+const MOZ_CAMERATESTHW_CONTRACTID = "@mozilla.org/cameratesthardware;1";
+const MOZ_CAMERATESTHW_CID        = Components.ID("{fcb7b4cd-689e-453c-8a2c-611a45fa09ac}");
+const DEBUG = false;
+
+function debug(msg) {
+  if (DEBUG) {
+    dump('-*- MozCameraTestHardware: ' + msg + '\n');
+  }
+}
+
+function MozCameraTestHardware() {
+  this._params = {};
+}
+
+MozCameraTestHardware.prototype = {
+  classID:        MOZ_CAMERATESTHW_CID,
+  contractID:     MOZ_CAMERATESTHW_CONTRACTID,
+
+  classInfo:      XPCOMUtils.generateCI({classID: MOZ_CAMERATESTHW_CID,
+                                         contractID: MOZ_CAMERATESTHW_CONTRACTID,
+                                         flags: Ci.nsIClassInfo.SINGLETON,
+                                         interfaces: [Ci.nsICameraTestHardware]}),
+
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsICameraTestHardware]),
+
+  _params: null,
+  _window: null,
+  _mock: null,
+  _handler: null,
+
+  attach: function(mock) {
+    /* Waive xrays permits us to call functions provided to us
+       in the mock */
+    this._mock = Components.utils.waiveXrays(mock);
+  },
+
+  detach: function() {
+    this._mock = null;
+  },
+
+  /* Trigger a delegate handler attached to the test hardware
+     if given via attach. If there is no delegate attached, or
+     it does not provide a handler for this specific operation,
+     or the handler returns true, it will execute the default
+     behaviour. The handler may throw an exception in order to
+     return an error code from the driver call. */
+  _delegate: function(prop) {
+    return (this._mock && this._mock[prop] && !this._mock[prop]());
+  },
+
+  get params() {
+    return this._params;
+  },
+
+  set params(aParams) {
+    this._params = aParams;
+  },
+
+  setHandler: function(handler) {
+    this._handler = handler;
+  },
+
+  dispatchEvent: function(evt) {
+    if (this._handler) {
+      this._handler.handleEvent(evt);
+    }
+  },
+
+  reset: function(aWindow) {
+    this._window = aWindow;
+    this._mock = null;
+    this._params = {};
+  },
+
+  initCamera: function() {
+    this._delegate('init');
+  },
+
+  pushParameters: function(params) {
+    let oldParams = this._params;
+    this._params = {};
+    let s = params.split(';');
+    for(let i = 0; i < s.length; ++i) {
+      let parts = s[i].split('=');
+      if (parts.length == 2) {
+        this._params[parts[0]] = parts[1];
+      }
+    }
+    try {
+      this._delegate('pushParameters');
+    } catch(e) {
+      this._params = oldParams;
+      throw e;
+    }
+  },
+
+  pullParameters: function() {
+    this._delegate('pullParameters');
+    let ret = "";
+    for(let p in this._params) {
+      ret += p + "=" + this._params[p] + ";";
+    }
+    return ret;
+  },
+
+  autoFocus: function() {
+    if (!this._delegate('autoFocus')) {
+      this.fireAutoFocusComplete(true);
+    }
+  },
+
+  fireAutoFocusMoving: function(moving) {
+    let evt = new this._window.CameraStateChangeEvent('focus', { 'newState': moving ? 'focusing' : 'not_focusing' } );
+    this.dispatchEvent(evt);
+  },
+
+  fireAutoFocusComplete: function(state) {
+    let evt = new this._window.CameraStateChangeEvent('focus', { 'newState': state ? 'focused' : 'unfocused' } );
+    this.dispatchEvent(evt);
+  },
+
+  cancelAutoFocus: function() {
+    this._delegate('cancelAutoFocus');
+  },
+
+  fireShutter: function() {
+    let evt = new this._window.Event('shutter');
+    this.dispatchEvent(evt);
+  },
+
+  takePicture: function() {
+    if (!this._delegate('takePicture')) {
+      this.fireTakePictureComplete(new this._window.Blob(['foobar'], {'type': 'jpeg'}));
+    }
+  },
+
+  fireTakePictureComplete: function(blob) {
+    let evt = new this._window.BlobEvent('picture', {'data': blob});
+    this.dispatchEvent(evt);
+  },
+
+  fireTakePictureError: function() {
+    let evt = new this._window.ErrorEvent('error', {'message': 'picture'});
+    this.dispatchEvent(evt);
+  },
+
+  cancelTakePicture: function() {
+    this._delegate('cancelTakePicture');
+  },
+
+  startPreview: function() {
+    this._delegate('startPreview');
+  },
+
+  stopPreview: function() {
+    this._delegate('stopPreview');
+  },
+
+  startFaceDetection: function() {
+    this._delegate('startFaceDetection');
+  },
+
+  stopFaceDetection: function() {
+    this._delegate('stopFaceDetection');
+  },
+
+  fireFacesDetected: function(faces) {
+    /* This works around the fact that we can't have references to
+       dictionaries in a dictionary in WebIDL; we provide a boolean
+       to indicate whether or not the values for those features are
+       actually valid. */
+    let facesIf = [];
+    if (typeof(faces) === 'object' && typeof(faces.faces) === 'object') {
+      let self = this;
+      faces.faces.forEach(function(face) {
+        face.hasLeftEye = face.hasOwnProperty('leftEye') && face.leftEye != null;
+        face.hasRightEye = face.hasOwnProperty('rightEye') && face.rightEye != null;
+        face.hasMouth = face.hasOwnProperty('mouth') && face.mouth != null;
+        facesIf.push(new self._window.CameraDetectedFace(face));
+      });
+    }
+
+    let evt = new this._window.CameraFacesDetectedEvent('facesdetected', {'faces': facesIf});
+    this.dispatchEvent(evt);
+  },
+
+  startRecording: function() {
+    this._delegate('startRecording');
+  },
+
+  stopRecording: function() {
+    this._delegate('stopRecording');
+  },
+
+  fireSystemError: function() {
+    let evt = new this._window.ErrorEvent('error', {'message': 'system'});
+    this.dispatchEvent(evt);
+  },
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([MozCameraTestHardware]);
new file mode 100644
--- /dev/null
+++ b/dom/camera/CameraTestHardware.manifest
@@ -0,0 +1,2 @@
+component {fcb7b4cd-689e-453c-8a2c-611a45fa09ac} CameraTestHardware.js
+contract @mozilla.org/cameratesthardware;1 {fcb7b4cd-689e-453c-8a2c-611a45fa09ac}
--- a/dom/camera/DOMCameraDetectedFace.cpp
+++ b/dom/camera/DOMCameraDetectedFace.cpp
@@ -26,16 +26,50 @@ DOMCameraDetectedFace::HasSupport(JSCont
 }
 
 JSObject*
 DOMCameraDetectedFace::WrapObject(JSContext* aCx)
 {
   return CameraDetectedFaceBinding::Wrap(aCx, this);
 }
 
+/* static */
+already_AddRefed<DOMCameraDetectedFace>
+DOMCameraDetectedFace::Constructor(const GlobalObject& aGlobal,
+                                   const dom::CameraDetectedFaceInit& aFace,
+                                   ErrorResult& aRv)
+{
+  nsCOMPtr<nsISupports> s = do_QueryInterface(aGlobal.GetAsSupports());
+  nsRefPtr<DOMCameraDetectedFace> face = new DOMCameraDetectedFace(s, aFace);
+  return face.forget();
+}
+
+DOMCameraDetectedFace::DOMCameraDetectedFace(nsISupports* aParent,
+                                             const dom::CameraDetectedFaceInit& aFace)
+  : mParent(aParent)
+  , mId(aFace.mId)
+  , mScore(aFace.mScore)
+  , mBounds(new DOMRect(this))
+{
+  mBounds->SetRect(aFace.mBounds.mLeft,
+                   aFace.mBounds.mTop,
+                   aFace.mBounds.mRight - aFace.mBounds.mLeft,
+                   aFace.mBounds.mBottom - aFace.mBounds.mTop);
+
+  if (aFace.mHasLeftEye) {
+    mLeftEye = new DOMPoint(this, aFace.mLeftEye.mX, aFace.mLeftEye.mY);
+  }
+  if (aFace.mHasRightEye) {
+    mRightEye = new DOMPoint(this, aFace.mRightEye.mX, aFace.mRightEye.mY);
+  }
+  if (aFace.mHasMouth) {
+    mMouth = new DOMPoint(this, aFace.mMouth.mX, aFace.mMouth.mY);
+  }
+}
+
 DOMCameraDetectedFace::DOMCameraDetectedFace(nsISupports* aParent,
                                              const ICameraControl::Face& aFace)
   : mParent(aParent)
   , mId(aFace.id)
   , mScore(aFace.score)
   , mBounds(new DOMRect(this))
 {
   mBounds->SetRect(aFace.bound.left,
--- a/dom/camera/DOMCameraDetectedFace.h
+++ b/dom/camera/DOMCameraDetectedFace.h
@@ -25,16 +25,20 @@ public:
 
   // Because this header's filename doesn't match its C++ or DOM-facing
   // classname, we can't rely on the [Func="..."] WebIDL tag to implicitly
   // include the right header for us; instead we must explicitly include a
   // HasSupport() method in each header. We can get rid of these with the
   // Great Renaming proposed in bug 983177.
   static bool HasSupport(JSContext* aCx, JSObject* aGlobal);
 
+  static already_AddRefed<DOMCameraDetectedFace> Constructor(const GlobalObject& aGlobal,
+                                                             const dom::CameraDetectedFaceInit& aFace,
+                                                             ErrorResult& aRv);
+
   DOMCameraDetectedFace(nsISupports* aParent, const ICameraControl::Face& aFace);
 
   uint32_t Id()       { return mId; }
   uint32_t Score()    { return mScore; }
   bool HasLeftEye()   { return mLeftEye; }
   bool HasRightEye()  { return mRightEye; }
   bool HasMouth()     { return mMouth; }
 
@@ -49,16 +53,17 @@ public:
   {
     MOZ_ASSERT(mParent);
     return mParent;
   }
 
   virtual JSObject* WrapObject(JSContext* aCx) MOZ_OVERRIDE;
 
 protected:
+  DOMCameraDetectedFace(nsISupports* aParent, const dom::CameraDetectedFaceInit& aFace);
   virtual ~DOMCameraDetectedFace() { }
 
   nsCOMPtr<nsISupports> mParent;
 
   uint32_t mId;
   uint32_t mScore;
 
   nsRefPtr<dom::DOMRect> mBounds;
--- a/dom/camera/DOMCameraManager.cpp
+++ b/dom/camera/DOMCameraManager.cpp
@@ -12,17 +12,16 @@
 #include "nsIContentPermissionPrompt.h"
 #include "nsIObserverService.h"
 #include "nsIPermissionManager.h"
 #include "nsIScriptObjectPrincipal.h"
 #include "DOMCameraControl.h"
 #include "nsDOMClassInfo.h"
 #include "CameraCommon.h"
 #include "mozilla/dom/BindingUtils.h"
-#include "mozilla/dom/CameraManagerBinding.h"
 #include "mozilla/dom/PermissionMessageUtils.h"
 
 using namespace mozilla;
 using namespace mozilla::dom;
 
 NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(nsDOMCameraManager, mWindow)
 
 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(nsDOMCameraManager)
--- a/dom/camera/moz.build
+++ b/dom/camera/moz.build
@@ -20,27 +20,38 @@ UNIFIED_SOURCES += [
     'DOMCameraCapabilities.cpp',
     'DOMCameraControl.cpp',
     'DOMCameraControlListener.cpp',
     'DOMCameraDetectedFace.cpp',
     'DOMCameraManager.cpp',
 ]
 
 if CONFIG['MOZ_B2G_CAMERA']:
+    XPIDL_SOURCES += [
+        'nsICameraTestHardware.idl',
+    ]
+
+    XPIDL_MODULE = 'dom_camera'
+
     UNIFIED_SOURCES += [
         'GonkCameraControl.cpp',
         'GonkCameraHwMgr.cpp',
         'GonkCameraManager.cpp',
         'GonkCameraParameters.cpp',
         'GonkCameraSource.cpp',
         'GonkRecorder.cpp',
         'GonkRecorderProfiles.cpp',
         'TestGonkCameraControl.cpp',
         'TestGonkCameraHardware.cpp',
     ]
+
+    EXTRA_COMPONENTS += [
+        'CameraTestHardware.js',
+        'CameraTestHardware.manifest',
+    ]
 else:
     UNIFIED_SOURCES += [
         'FallbackCameraControl.cpp',
         'FallbackCameraManager.cpp',
     ]
 
 FAIL_ON_WARNINGS = True
 
new file mode 100644
--- /dev/null
+++ b/dom/camera/nsICameraTestHardware.idl
@@ -0,0 +1,191 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+interface nsIDOMBlob;
+interface nsIDOMEventListener;
+
+[scriptable, uuid(2e567730-f164-49d7-b975-862caa4425a5)]
+interface nsICameraTestHardware : nsISupports
+{
+  /* The following methods are intended to be used by the test cases
+     written in JavaScript to define the behaviour of the hardware: */
+
+  /* Attach a delegate handler object such that the test hardware
+     will call the given handlers for the given operations to decide
+     what to do. This allows a test case to define specific behaviours
+     on a fine grained basis.
+
+     The following handlers may be supplied as properties of the
+     given delagate handler object:
+       autoFocus
+       cancelAutoFocus
+       cancelTakePicture
+       init
+       pushParameters
+       pullParameters
+       startFaceDetection
+       startPreview
+       startRecording
+       stopFaceDetection
+       stopPreview
+       stopRecording
+       takePicture
+
+     Implementation notes for handlers:
+
+     - If the handler throws an error, we will the return code
+       of the driver operation.
+
+     - If the handler returns true, we will perform the default
+       action (if any) for the operation. */
+  void attach(in jsval mock);
+
+  /* Detach a delegate handler object such that the test hardware
+     will revert to default behaviour when a function is called. */
+  void detach();
+
+  /* Reset the state of the test hardware back to the initial state.
+     This is useful when one test case has been completed and we need
+     a clean slate for the next. */
+  void reset(in jsval window);
+
+  /* Trigger an OnAutoFocusMoving callback at the Gonk layer.
+
+     state is a boolean indicating where or not the camera focus
+     is moving. */
+  void fireAutoFocusComplete(in boolean state);
+
+  /* Trigger an OnAutoFocusComplete callback at the Gonk layer.
+
+     state is a boolean indicating where or not the camera is focused. */
+  void fireAutoFocusMoving(in boolean moving);
+
+  /* Trigger an OnTakePictureComplete callback at the Gonk layer.
+
+     blob should be a Blob object. The actual content of the blob
+     is unimportant since nothing processes it as an image internally. */
+  void fireTakePictureComplete(in nsIDOMBlob picture);
+
+  /* Trigger an OnTakePictureError callback at the Gonk layer. */
+  void fireTakePictureError();
+
+  /* Trigger an OnSystemError callback at the Gonk layer. */
+  void fireSystemError();
+
+  /* Trigger an OnShutter callback at the Gonk layer. */
+  void fireShutter();
+
+  /* Trigger an OnFacesDetected callback at the Gonk layer.
+
+     faces is an array of CameraDetectedFaceInit dictionaries although
+     hasLeftEye, hasRightEye and hasMouth may be omitted and will be
+     implied by the presence/absence of leftEye, rightEye and mouth. */
+  void fireFacesDetected(in jsval faces);
+
+  /* Object which stores the camera parameters read/written by the
+     camera control layer from the hardware. The test case may set
+     its own values to control the behaviour of the camera middleware.
+
+     E.g. params['preview-sizes'] = '320x240,640x480'; */
+  attribute jsval params;
+
+  /* The following methods are intended to be used by the Gonk layer
+     in order to call back into JavaScript to get test case defined
+     behaviour: */
+
+  /* Set a handler to capture asynchronous events triggered by the
+     test case via the fireXXX methods. E.g.:
+
+       nsCOMPtr<nsICameraHardware> wrapper =
+         do_GetService("@mozilla.org/cameratesthardware;1");
+
+       nsCOMPtr<nsIDOMEventListener> listener = new HwListener();
+
+       wrapper->setHander(listener);
+
+     where
+
+       class HwListener : public nsIDOMEventListener {
+         NS_IMETHODIMP HandleEvent(nsIDOMEvent *aEvent) {
+           nsString type;
+           aEvent->GetType(&type);
+           if (aEvent.EqualsLiteral("focus")) {
+              ...
+           } else {
+              ...
+           }
+         }
+       };
+
+     The following event types may be generated:
+       focus: CameraStateChangeEvent where newState should map
+       to the OnAutoFocusComplete and OnAutoFocusMoving callbacks:
+       -- focused: OnAutoFocusComplete(false)
+       -- unfocused: OnAutoFocusComplete(true)
+       -- focusing: OnAutoFocusMoving(true)
+       -- not_focusing: OnAutoFocusMoving(false)
+
+       picture: BlobEvent which contains the picture type and
+       data corresponding to the OnTakePictureComplete callback.
+
+       error: ErrorEvent corresponding to the various error callbacks,
+       where the message is:
+       -- picture: OnTakePictureError()
+       -- system: OnSystemError(100, 0)
+
+       facesdetected: CameraFacesDetectedEvent which contains the
+       faces data corresponding to OnFacesDetected callback.
+
+       shutter: Event which corresponds to the OnShutter callback. */
+  void setHandler(in nsIDOMEventListener handler);
+
+  /* Execute an intercepted Init() driver call. */
+  void initCamera();
+
+  /* Execute an intercepted AutoFocus() driver call. Default behaviour is
+     to trigger OnAutoFocusComplete where the camera is focused. */
+  void autoFocus();
+
+  /* Execute an intercepted CancelAutoFocus() driver call. */
+  void cancelAutoFocus();
+
+  /* Execute an intercepted StartFaceDetection() driver call. */
+  void startFaceDetection();
+
+  /* Execute an intercepted StopFaceDetection() driver call. */
+  void stopFaceDetection();
+
+  /* Execute an intercepted TakePicture() driver call. Default behaviour is
+     to trigger OnTakePictureComplete with a fake jpeg blob. */
+  void takePicture();
+
+  /* Execute an intercepted CancelTakePicture() driver call. */
+  void cancelTakePicture();
+
+  /* Execute an intercepted StartPreview() driver call. */
+  void startPreview();
+
+  /* Execute an intercepted StopPreview() driver call. */
+  void stopPreview();
+
+  /* Execute an intercepted StartRecording() driver call. */
+  void startRecording();
+
+  /* Execute an intercepted StopRecording() driver call. */
+  void stopRecording();
+
+  /* Execute an intercepted PushParameters() driver call. If the delegate
+     handler throws an error, it will restore the old parameters.
+     When the delegate is called, the new proposed parameters are
+     placed in this.params. */
+  void pushParameters(in DOMString params);
+
+  /* Execute an intercepted PullParameters() driver call. Unless the delegate
+     handler throws an error, it will return an assembled parameter
+     list derived from the this.params hash table. */
+  DOMString pullParameters();
+};
+
--- a/dom/camera/test/camera_common.js
+++ b/dom/camera/test/camera_common.js
@@ -1,169 +1,396 @@
-var CameraTest = (function() {
-  'use strict';
+function isDefinedObj(obj) {
+  return typeof(obj) !== 'undefined' && obj != null;
+}
+
+function isDefined(obj) {
+  return typeof(obj) !== 'undefined';
+}
+
+/* This is a simple test suite class removing the need to
+   write a lot of boilerplate for camera tests. It can
+   manage the platform configurations for testing, any
+   cleanup required, and common actions such as fetching
+   the camera or waiting for the preview to be completed.
+
+   To create the suite:
+     var suite = new CameraTestSuite();
+
+   To add a test case to the suite:
+     suite.test('test-name', function() {
+       function startAutoFocus(p) {
+         return suite.camera.autoFocus();
+       }
+
+       return suite.getCamera()
+         .then(startAutoFocus, suite.rejectGetCamera);
+     });
+
+   Finally, to execute the test cases:
+     suite.setup()
+       .then(suite.run);
+
+   Behind the scenes, suite configured the native camera
+   to use the JS hardware, setup that hardware such that
+   the getCamera would succeed, got a camera control
+   reference and saved it to suite.camera, and after the
+   tests were finished, it reset any modified state,
+   released the camera object, and concluded the mochitest
+   appropriately.
+*/
+function CameraTestSuite() {
+  SimpleTest.waitForExplicitFinish();
+
+  this._window = window;
+  this._document = document;
+  this.viewfinder = document.getElementById('viewfinder');
+  this._tests = [];
+  this.hwType = '';
+
+  /* Ensure that the this pointer is bound to all functions so that
+     they may be used as promise resolve/reject handlers without any
+     special effort, permitting code like this:
+
+       getCamera().catch(suite.rejectGetCamera);
+
+     instead of:
 
-  /**
-   * 'camera.control.test.enabled' is queried in Gecko to enable different
-   * test modes in the camera stack. The only currently-supported setting
-   * is 'hardware', which wraps the Gonk camera abstraction class in a
-   * shim class that supports injecting hardware camera API failures into
-   * the execution path.
-   *
-   * The affected API is specified by the 'camera.control.test.hardware'
-   * pref. Currently supported values should be determined by inspecting
-   * TestGonkCameraHardware.cpp.
-   *
-   * Some API calls are simple: e.g. 'start-recording-failure' will cause
-   * the DOM-facing startRecording() call to fail. More complex tests like
-   * 'take-picture-failure' will cause the takePicture() API to fail, while
-   * 'take-picture-process-failure' will simulate a failure of the
-   * asynchronous picture-taking process, even if the initial API call
-   * path seems to have succeeded.
-   *
-   * If 'camera.control.test.hardware.gonk.parameters' is set, it will cause
-   * the contents of that string to be appended to the string of parameters
-   * pulled from the Gonk camera library. This allows tests to inject fake
-   * settings/capabilities for features not supported by the emulator. These
-   * parameters are one or more semicolon-delimited key=value pairs, e.g. to
-   * pretend the emulator supports zoom:
-   *
-   *   zoom-ratios=100,150,200,300,400;max-zoom=4
-   *
-   * This means (of course) that neither the key not the value tokens can
-   * contain either equals signs or semicolons. The test shim doesn't enforce
-   * this so that we can test getting junk from the camera library as well.
-   */
-  const PREF_TEST_ENABLED = "camera.control.test.enabled";
-  const PREF_TEST_HARDWARE = "camera.control.test.hardware";
-  const PREF_TEST_EXTRA_PARAMETERS = "camera.control.test.hardware.gonk.parameters";
-  const PREF_TEST_FAKE_LOW_MEMORY = "camera.control.test.is_low_memory";
-  var oldTestEnabled;
-  var oldTestHw;
-  var testMode;
+       getCamera().catch(suite.rejectGetCamera.bind(suite));
+  */
+  this.setup = this._setup.bind(this);
+  this.teardown = this._teardown.bind(this);
+  this.test = this._test.bind(this);
+  this.run = this._run.bind(this);
+  this.waitPreviewStarted = this._waitPreviewStarted.bind(this);
+  this.waitParameterPush = this._waitParameterPush.bind(this);
+  this.initJsHw = this._initJsHw.bind(this);
+  this.getCamera = this._getCamera.bind(this);
+  this.setLowMemoryPlatform = this._setLowMemoryPlatform.bind(this);
+  this.logError = this._logError.bind(this);
+  this.expectedError = this._expectedError.bind(this);
+  this.expectedRejectGetCamera = this._expectedRejectGetCamera.bind(this);
+  this.expectedRejectAutoFocus = this._expectedRejectAutoFocus.bind(this);
+  this.expectedRejectTakePicture = this._expectedRejectTakePicture.bind(this);
+  this.rejectGetCamera = this._rejectGetCamera.bind(this);
+  this.rejectRelease = this._rejectRelease.bind(this);
+  this.rejectAutoFocus = this._rejectAutoFocus.bind(this);
+  this.rejectTakePicture = this._rejectTakePicture.bind(this);
+  this.rejectPreviewStarted = this._rejectPreviewStarted.bind(this);
+
+  var self = this;
+  this._window.addEventListener('beforeunload', function() {
+    if (isDefinedObj(self.viewfinder)) {
+      self.viewfinder.mozSrcObject = null;
+    }
+
+    self.hw = null;
+    if (isDefinedObj(self.camera)) {
+      ok(false, 'window unload triggered camera release instead of test completion');
+      self.camera.release();
+      self.camera = null;
+    }
+  });
+}
 
-  function testHardwareSetFakeParameters(parameters, callback) {
-    SpecialPowers.pushPrefEnv({'set': [[PREF_TEST_EXTRA_PARAMETERS, parameters]]}, function() {
-      var setParams = SpecialPowers.getCharPref(PREF_TEST_EXTRA_PARAMETERS);
-      ise(setParams, parameters, "Extra test parameters '" + setParams + "'");
-      if (callback) {
-        callback(setParams);
-      }
+CameraTestSuite.prototype = {
+  camera: null,
+  hw: null,
+  _lowMemSet: false,
+
+  /* Returns a promise which is resolved when the test suite is ready
+     to be executing individual test cases. One may provide the expected
+     hardware type here if desired; the default is to use the JS test
+     hardware. Use '' for the native emulated camera hardware. */
+  _setup: function(hwType) {
+    if (!isDefined(hwType)) {
+      hwType = 'hardware';
+    }
+
+    this._hwType = hwType;
+    return new Promise(function(resolve, reject) {
+      SpecialPowers.pushPrefEnv({'set': [['camera.control.test.enabled', hwType]]}, function() {
+        resolve();
+      });
     });
-  }
+  },
+
+  /* Returns a promise which is resolved when all of the SpecialPowers
+     parameters that were set while testing are flushed. This includes
+     camera.control.test.enabled and camera.control.test.is_low_memory. */
+  _teardown: function() {
+    return new Promise(function(resolve, reject) {
+      SpecialPowers.flushPrefEnv(function() {
+        resolve();
+      });
+    });
+  },
 
-  function testHardwareClearFakeParameters(callback) {
-    SpecialPowers.pushPrefEnv({'clear': [[PREF_TEST_EXTRA_PARAMETERS]]}, callback);
-  }
+  /* Returns a promise which is resolved when the set low memory
+     parameter is set. If no value is given, it defaults to true.
+     This is intended to be used inside a test case at the beginning
+     of its promise chain to configure the platform as desired. */
+  _setLowMemoryPlatform: function(val) {
+    if (typeof(val) === 'undefined') {
+      val = true;
+    }
+
+    if (this._lowMemSet === val) {
+      return Promise.resolve();
+    }
 
-  function testHardwareSetFakeLowMemoryPlatform(callback) {
-    SpecialPowers.pushPrefEnv({'set': [[PREF_TEST_FAKE_LOW_MEMORY, true]]}, function() {
-      var setParams = SpecialPowers.getBoolPref(PREF_TEST_FAKE_LOW_MEMORY);
-      ise(setParams, true, "Fake low memory platform");
-      if (callback) {
-        callback(setParams);
+    var self = this;
+    return new Promise(function(resolve, reject) {
+      SpecialPowers.pushPrefEnv({'set': [['camera.control.test.is_low_memory', val]]}, function() {
+        self._lowMemSet = val;
+        resolve();
+      });
+    }).catch(function(e) {
+      return self.logError('set low memory ' + val + ' failed', e);
+    });
+  },
+
+  /* Add a test case to the test suite to be executed later. */
+  _test: function(aName, aCb) {
+    this._tests.push({
+      name: aName,
+      cb: aCb
+    });
+  },
+
+  /* Execute all test cases (after setup is called). */
+  _run: function() {
+    var test = this._tests.shift();
+    var self = this;
+    if (test) {
+      info(test.name + ' started');
+
+      function runNextTest() {
+        self.run();
       }
-    });
-  }
 
-  function testHardwareClearFakeLowMemoryPlatform(callback) {
-    SpecialPowers.pushPrefEnv({'clear': [[PREF_TEST_FAKE_LOW_MEMORY]]}, callback);
-  }
+      function resetLowMem() {
+        return self.setLowMemoryPlatform(false);
+      }
+
+      function postTest(pass) {
+        ok(pass, test.name + ' finished');
+        var camera = self.camera;
+        self.viewfinder.mozSrcObject = null;
+        self.camera = null;
+
+        if (!isDefinedObj(camera)) {
+          return Promise.resolve();
+        }
 
-  function testHardwareSet(test, callback) {
-    SpecialPowers.pushPrefEnv({'set': [[PREF_TEST_HARDWARE, test]]}, function() {
-      var setTest = SpecialPowers.getCharPref(PREF_TEST_HARDWARE);
-      ise(setTest, test, "Test subtype set to " + setTest);
-      if (callback) {
-        callback(setTest);
+        function handler(e) {
+          ok(typeof(e) === 'undefined', 'camera released');
+          return Promise.resolve();
+        }
+
+        return camera.release().then(handler).catch(handler);
       }
-    });
-  }
+
+      this.initJsHw();
+
+      var testPromise;
+      try {
+        testPromise = test.cb();
+        if (!isDefinedObj(testPromise)) {
+          testPromise = Promise.resolve();
+        }
+      } catch(e) {
+        ok(false, 'caught exception while running test: ' + e);
+        testPromise = Promise.reject(e);
+      }
 
-  function testHardwareDone(callback) {
-    testMode = null;
-    if (oldTestHw) {
-      SpecialPowers.pushPrefEnv({'set': [[PREF_TEST_HARDWARE, oldTestHw]]}, callback);
+      testPromise
+        .then(function(p) {
+          return postTest(true);
+        }, function(e) {
+          self.logError('unhandled error', e);
+          return postTest(false);
+        })
+        .then(resetLowMem, resetLowMem)
+        .then(runNextTest, runNextTest);
     } else {
-      SpecialPowers.pushPrefEnv({'clear': [[PREF_TEST_HARDWARE]]}, callback);
+      ok(true, 'all tests completed');
+      var finish = SimpleTest.finish.bind(SimpleTest);
+      this.teardown().then(finish, finish);
     }
-  }
+  },
+
+  /* If the JS hardware is in use, get (and possibly initialize)
+     the service XPCOM object. The native Gonk layers are able
+     to get it via the same mechanism. Save a reference to it
+     so that the test case may manipulate it as it sees fit in
+     this.hw. Minimal setup is done for the test hardware such
+     that the camera is able to be brought up without issue.
+
+     This function has no effect if the JS hardware is not used. */
+  _initJsHw: function() {
+    if (this._hwType === 'hardware') {
+      this.hw = SpecialPowers.Cc['@mozilla.org/cameratesthardware;1']
+                .getService(SpecialPowers.Ci.nsICameraTestHardware);
+      this.hw.reset(this._window);
+
+      /* Minimum parameters required to get camera started */
+      this.hw.params['preview-size'] = '320x240';
+      this.hw.params['preview-size-values'] = '320x240';
+      this.hw.params['picture-size-values'] = '320x240';
+    } else {
+      this.hw = null;
+    }
+  },
 
-  function testBegin(mode, callback) {
-    SimpleTest.waitForExplicitFinish();
-    try {
-      oldTestEnabled = SpecialPowers.getCharPref(PREF_TEST_ENABLED);
-    } catch(e) { }
-    SpecialPowers.pushPrefEnv({'set': [[PREF_TEST_ENABLED, mode]]}, function() {
-      var setMode = SpecialPowers.getCharPref(PREF_TEST_ENABLED);
-      ise(setMode, mode, "Test mode set to " + setMode);
-      if (setMode === "hardware") {
+  /* Returns a promise which resolves when the camera has
+     been successfully opened with the given name and
+     configuration. If no name is given, it uses the first
+     camera in the list from the camera manager. */
+  _getCamera: function(name, config) {
+    var cameraManager = navigator.mozCameras;
+    if (!isDefined(name)) {
+      name = cameraManager.getListOfCameras()[0];
+    }
+
+    var self = this;
+    return cameraManager.getCamera(name, config).then(
+      function(p) {
+        ok(isDefinedObj(p) && isDefinedObj(p.camera), 'got camera');
+        self.camera = p.camera;
+        /* Ensure a followup promise can verify config by
+           returning the same parameter again. */
+        return Promise.resolve(p);
+      }
+    );
+  },
+
+  /* Returns a promise which resolves when the camera has
+     successfully started the preview and is bound to the
+     given viewfinder object. Note that this requires that
+     a video element be present with the ID 'viewfinder'. */
+  _waitPreviewStarted: function() {
+    var self = this;
+
+    return new Promise(function(resolve, reject) {
+      function onPreviewStateChange(e) {
         try {
-          oldTestHw = SpecialPowers.getCharPref(PREF_TEST_HARDWARE);
-        } catch(e) { }
-        testMode = {
-          set: testHardwareSet,
-          setFakeParameters: testHardwareSetFakeParameters,
-          clearFakeParameters: testHardwareClearFakeParameters,
-          setFakeLowMemoryPlatform: testHardwareSetFakeLowMemoryPlatform,
-          clearFakeLowMemoryPlatform: testHardwareClearFakeLowMemoryPlatform,
-          done: testHardwareDone
-        };
-        if (callback) {
-          callback(testMode);
+          if (e.newState === 'started') {
+            ok(true, 'viewfinder is ready and playing');
+            self.camera.removeEventListener('previewstatechange', onPreviewStateChange);
+            resolve();
+          }
+        } catch(e) {
+          reject(e);
         }
       }
-    });
-  }
 
-  function testEnd(callback) {
-    // A chain of clean-up functions....
-    function allCleanedUp() {
-      SimpleTest.finish();
-      if (callback) {
-        callback();
-      }
-    }
-
-    function cleanUpTestEnabled() {
-      var next = allCleanedUp;
-      if (oldTestEnabled) {
-        SpecialPowers.pushPrefEnv({'set': [[PREF_TEST_ENABLED, oldTestEnabled]]}, next);
-      } else {
-        SpecialPowers.pushPrefEnv({'clear': [[PREF_TEST_ENABLED]]}, next);
-      }
-    }
-    function cleanUpTest() {
-      var next = cleanUpTestEnabled;
-      if (testMode) {
-        testMode.done(next);
-        testMode = null;
-      } else {
-        next();
+      if (!isDefinedObj(self.viewfinder)) {
+        reject(new Error('no viewfinder object'));
+        return;
       }
-    }
-    function cleanUpLowMemoryPlatform() {
-      var next = cleanUpTest;
-      if (testMode) {
-        testMode.clearFakeLowMemoryPlatform(next);
-      } else {
-        next();
-      }
+
+      self.viewfinder.mozSrcObject = self.camera;
+      self.viewfinder.play();
+      self.camera.addEventListener('previewstatechange', onPreviewStateChange);
+    });
+  },
+
+  /* Returns a promise which resolves when the camera hardware
+     has received a push parameters request. This is useful
+     when setting camera parameters from the application and
+     you want confirmation when the operation is complete if
+     there is no asynchronous notification provided. */
+  _waitParameterPush: function() {
+    var self = this;
+
+    return new Promise(function(resolve, reject) {
+      self.hw.attach({
+        'pushParameters': function() {
+          self._window.setTimeout(resolve);
+        }
+      });
+    });
+  },
+
+  /* When an error occurs in the promise chain, all of the relevant rejection
+     functions will be triggered. Most of the time however we only want the
+     first rejection to be handled and then let the failure trickle down the
+     chain to terminate the test. There is no way to exit a promise chain
+     early so the convention is to handle the error in the first reject and
+     then give an empty error for subsequent reject handlers so they know
+     it is not for them.
+
+     For example:
+       function rejectSomething(e) {
+         return suite.logError('something call failed');
+       }
+
+       getCamera()
+         .then(, suite.rejectGetCamera)
+         .then(something)
+         .then(, rejectSomething)
+
+     If the getCamera promise is rejected, suite.rejectGetCamera reports an
+     error, but rejectSomething remains silent. */
+  _logError: function(msg, e) {
+    if (isDefined(e)) {
+      ok(false, msg + ': ' + e);
     }
-    function cleanUpExtraParameters() {
-      var next = cleanUpLowMemoryPlatform;
-      if (testMode) {
-        testMode.clearFakeParameters(next);
-      } else {
-        next();
-      }
-    }
+    // Make sure the error is undefined for later handlers
+    return Promise.reject();
+  },
+
+  /* The reject handlers below are intended to be used
+     when a test case does not expect a particular call
+     to fail but otherwise does not require any special
+     handling of that situation beyond failing the test
+     case and logging why.*/
+  _rejectGetCamera: function(e) {
+    return this.logError('get camera failed', e);
+  },
+
+  _rejectRelease: function(e) {
+    return this.logError('release camera failed', e);
+  },
+
+  _rejectAutoFocus: function(e) {
+    return this.logError('auto focus failed', e);
+  },
+
+  _rejectTakePicture: function(e) {
+    return this.logError('take picture failed', e);
+  },
+
+  _rejectPreviewStarted: function(e) {
+    return this.logError('preview start failed', e);
+  },
 
-    cleanUpExtraParameters();
-  }
+  /* The success handlers below are intended to be used
+     when a test case does not expect a particular call
+     to succed but otherwise does not require any special
+     handling of that situation beyond failing the test
+     case and logging why.*/
+  _expectedError: function(msg) {
+    ok(false, msg);
+    /* Since the original promise was technically resolved
+       we actually want to pass up a rejection to try and
+       end the test case sooner */
+    return Promise.reject();
+  },
 
-  ise(SpecialPowers.sanityCheck(), "foo", "SpecialPowers passed sanity check");
-  return {
-    begin: testBegin,
-    end: testEnd
-  };
+  _expectedRejectGetCamera: function(p) {
+    /* Copy handle to ensure it gets released at the end
+       of the test case */
+    self.camera = p.camera;
+    return this.expectedError('expected get camera to fail');
+  },
 
-})();
+  _expectedRejectAutoFocus: function(p) {
+    return this.expectedError('expected auto focus to fail');
+  },
+
+  _expectedRejectTakePicture: function(p) {
+    return this.expectedError('expected take picture to fail');
+  },
+};
+
+ise(SpecialPowers.sanityCheck(), "foo", "SpecialPowers passed sanity check");
--- a/dom/webidl/CameraControl.webidl
+++ b/dom/webidl/CameraControl.webidl
@@ -418,17 +418,19 @@ interface CameraControl : MediaStream
        { x:  1000, y:  1000 } is the bottom-right corner
 
    'rightEye' is the coordinates of the detected right eye; null if not
    supported or detected. Same boundary conditions as 'leftEye'.
 
    'mouth' is the coordinates of the detected mouth; null if not supported or
    detected. Same boundary conditions as 'leftEye'.
 */
-[Pref="camera.control.face_detection.enabled", Func="DOMCameraDetectedFace::HasSupport"]
+[Pref="camera.control.face_detection.enabled",
+ Func="DOMCameraDetectedFace::HasSupport",
+ Constructor(optional CameraDetectedFaceInit initDict)]
 interface CameraDetectedFace
 {
   readonly attribute unsigned long id;
 
   readonly attribute unsigned long score;
 
   readonly attribute DOMRect bounds;
 
@@ -437,16 +439,29 @@ interface CameraDetectedFace
 
   readonly attribute boolean hasRightEye;
   readonly attribute DOMPoint? rightEye;
 
   readonly attribute boolean hasMouth;
   readonly attribute DOMPoint? mouth;
 };
 
+dictionary CameraDetectedFaceInit
+{
+  unsigned long id = 0;
+  unsigned long score = 100;
+  CameraRegion bounds;
+  boolean hasLeftEye = false;
+  DOMPointInit leftEye;
+  boolean hasRightEye = false;
+  DOMPointInit rightEye;
+  boolean hasMouth = false;
+  DOMPointInit mouth;
+};
+
 callback CameraFaceDetectionCallback = void (sequence<CameraDetectedFace> faces);
 
 partial interface CameraControl
 {
   /* Starts the face detection. This should be called after the preview is
      started. The camera will periodically call 'onFacesDetected' with a
      sequence of zero or one or more detected faces in the preview frame.