Bug 802694: Pass along constraints from PC JS module to PCImpl; r=ekr,jesup
authorAnant Narayanan <anant@kix.in>
Thu, 25 Oct 2012 12:24:30 -0700
changeset 111563 8af5442088923f704aa4463970abcacd718643f5
parent 111562 cc081e8dab229058245045431a545d22c3e91f62
child 111564 54591f4e0f792eb796228b6577a2be3bfce0af13
push id93
push usernmatsakis@mozilla.com
push dateWed, 31 Oct 2012 21:26:57 +0000
reviewersekr, jesup
bugs802694
milestone19.0a1
Bug 802694: Pass along constraints from PC JS module to PCImpl; r=ekr,jesup
dom/media/PeerConnection.js
dom/media/bridge/IPeerConnection.idl
media/webrtc/signaling/src/peerconnection/PeerConnectionImpl.cpp
media/webrtc/signaling/src/peerconnection/PeerConnectionImpl.h
--- a/dom/media/PeerConnection.js
+++ b/dom/media/PeerConnection.js
@@ -225,153 +225,150 @@ PeerConnection.prototype = {
       if (!obj.wait) {
         this._executeNext();
       }
     } else {
       this._pending = false;
     }
   },
 
+  /**
+   * Constraints look like this:
+   *
+   * {
+   *   mandatory: {"foo": true, "bar": 10, "baz": "boo"},
+   *   optional: [{"foo": true}, {"bar": 10}]
+   * }
+   *
+   * We check for basic structure but not the validity of the constraints
+   * themselves before passing them along to C++.
+   */
+  _validateConstraints: function(constraints) {
+    function isObject(obj) {
+      return obj && (typeof obj === "object");
+    }
+    function isArray(obj) {
+      return isObject(obj) &&
+        (Object.prototype.toString.call(obj) === "[object Array]");
+    }
+
+    if (!isObject(constraints)) {
+      return false;
+    }
+    if (constraints.mandatory && !isObject(constraints.mandatory)) {
+      return false;
+    }
+    if (constraints.optional && !isArray(constraints.optional)) {
+      return false;
+    }
+
+    return true;
+  },
+
   createOffer: function(onSuccess, onError, constraints) {
     if (this._onCreateOfferSuccess) {
-      if (onError) {
-        onError.onCallback("createOffer already called");
-      }
-      return;
+      throw new Error("createOffer already called");
+    }
+
+    if (!this._validateConstraints(constraints)) {
+      throw new Error("createOffer passed invalid constraints");
     }
 
     this._onCreateOfferSuccess = onSuccess;
     this._onCreateOfferFailure = onError;
 
-    // TODO: Implement constraints/hints.
-    if (!constraints) {
-      constraints = "";
-    }
-
     this._queueOrRun({
       func: this._pc.createOffer,
       args: [constraints],
       wait: true
     });
   },
 
   createAnswer: function(onSuccess, onError, constraints, provisional) {
     if (this._onCreateAnswerSuccess) {
-      if (onError) {
-        try {
-          onError.onCallback("createAnswer already called");
-        } catch(e) {}
-      }
-      return;
+      throw new Error("createAnswer already called");
     }
 
     if (!this.remoteDescription) {
-      if (onError) {
-        try {
-          onError.onCallback("setRemoteDescription not called");
-        } catch(e) {}
-      }
+      throw new Error("setRemoteDescription not called");
     }
 
     if (this.remoteDescription.type != "offer") {
-      if (onError) {
-        try {
-          onError.onCallback("No outstanding offer");
-        } catch(e) {}
-      }
+      throw new Error("No outstanding offer");
+    }
+
+    if (!this._validateConstraints(constraints)) {
+      throw new Error("createAnswer passed invalid constraints");
     }
 
     this._onCreateAnswerSuccess = onSuccess;
     this._onCreateAnswerFailure = onError;
 
-    if (!constraints) {
-      constraints = "";
-    }
     if (!provisional) {
       provisional = false;
     }
 
-    // TODO: Implement provisional answer & constraints.
+    // TODO: Implement provisional answer.
     this._queueOrRun({
       func: this._pc.createAnswer,
-      args: ["", this.remoteDescription.sdp],
+      args: [constraints],
       wait: true
     });
   },
 
   setLocalDescription: function(desc, onSuccess, onError) {
     if (this._onSetLocalDescriptionSuccess) {
-      if (onError) {
-        try {
-          onError.onCallback("setLocalDescription already called");
-        } catch(e) {}
-      }
-      return;
+      throw new Error("setLocalDescription already called");
     }
 
     this._onSetLocalDescriptionSuccess = onSuccess;
     this._onSetLocalDescriptionFailure = onError;
 
     let type;
     switch (desc.type) {
       case "offer":
         type = Ci.IPeerConnection.kActionOffer;
         break;
       case "answer":
         type = Ci.IPeerConnection.kActionAnswer;
         break;
       default:
-        if (onError) {
-          try {
-            onError.onCallback(
-              "Invalid type " + desc.type + " provided to setLocalDescription"
-            );
-          } catch(e) {}
-          return;
-        }
+        throw new Error(
+          "Invalid type " + desc.type + " provided to setLocalDescription"
+        );
         break;
     }
 
     this._queueOrRun({
       func: this._pc.setLocalDescription,
       args: [type, desc.sdp],
       wait: true
     });
   },
 
   setRemoteDescription: function(desc, onSuccess, onError) {
     if (this._onSetRemoteDescriptionSuccess) {
-      if (onError) {
-        try {
-          onError.onCallback("setRemoteDescription already called");
-        } catch(e) {}
-      }
-      return;
+      throw new Error("setRemoteDescription already called");
     }
 
     this._onSetRemoteDescriptionSuccess = onSuccess;
     this._onSetRemoteDescriptionFailure = onError;
 
     let type;
     switch (desc.type) {
       case "offer":
         type = Ci.IPeerConnection.kActionOffer;
         break;
       case "answer":
         type = Ci.IPeerConnection.kActionAnswer;
         break;
       default:
-        if (onError) {
-          try {
-            onError.onCallback(
-              "Invalid type " + desc.type + " provided to setRemoteDescription"
-            );
-          } catch(e) {}
-          return;
-        }
+        throw new Error(
+          "Invalid type " + desc.type + " provided to setRemoteDescription"
+        );
         break;
     }
 
     this.remoteDescription = {
       type: desc.type, sdp: desc.sdp,
       __exposedProps__: { type: "rw", sdp: "rw" }
     };
 
--- a/dom/media/bridge/IPeerConnection.idl
+++ b/dom/media/bridge/IPeerConnection.idl
@@ -1,10 +1,11 @@
 #include "nsIThread.idl"
 #include "nsIDOMWindow.idl"
+#include "nsIPropertyBag2.idl"
 
 interface nsIDOMMediaStream;
 interface nsIDOMDataChannel;
 
 /* Do not confuse with nsIDOMRTCPeerConnection. This interface is purely for
  * communication between the PeerConnection JS DOM binding and the C++
  * implementation in SIPCC.
  *
@@ -46,17 +47,17 @@ interface IPeerConnectionObserver : nsIS
   /* When SDP is parsed and a candidate line is found this method is called.
    * It should hook back into the media transport to notify it of ICE candidates
    * listed in the SDP PeerConnectionImpl does not parse ICE candidates, just
    * pulls them out of the SDP.
    */
   void foundIceCandidate(in string candidate);
 };
 
-[scriptable, uuid(cb3f0048-1009-11e2-b822-87ee49eface7)]
+[scriptable, uuid(f6819246-f5af-40f2-ab82-e166d5da7ba0)]
 interface IPeerConnection : nsISupports
 {
   const unsigned long kHintAudio = 0x00000001;
   const unsigned long kHintVideo = 0x00000002;
 
   const long kActionNone = -1;
   const long kActionOffer = 0;
   const long kActionAnswer = 1;
@@ -68,18 +69,18 @@ interface IPeerConnection : nsISupports
   const long kIceConnected = 3;
   const long kIceFailed = 4;
 
   /* Must be called first. Observer events will be dispatched on the thread provided */
   void initialize(in IPeerConnectionObserver observer, in nsIDOMWindow window,
                   [optional] in nsIThread thread);
 
   /* JSEP calls */
-  void createOffer(in string hints);
-  void createAnswer(in string hints, in string offer);
+  [implicit_jscontext] void createOffer(in jsval constraints);
+  [implicit_jscontext] void createAnswer(in jsval constraints);
   void setLocalDescription(in long action, in string sdp);
   void setRemoteDescription(in long action, in string sdp);
 
   /* Adds the stream created by GetUserMedia */
   void addStream(in nsIDOMMediaStream stream);
   void removeStream(in nsIDOMMediaStream stream);
   void closeStreams();
 
--- a/media/webrtc/signaling/src/peerconnection/PeerConnectionImpl.cpp
+++ b/media/webrtc/signaling/src/peerconnection/PeerConnectionImpl.cpp
@@ -10,24 +10,29 @@
 #include "CSFLogStream.h"
 #include "ccapi_call_info.h"
 #include "CC_SIPCCCallInfo.h"
 #include "ccapi_device_info.h"
 #include "CC_SIPCCDeviceInfo.h"
 #include "cpr_string.h"
 #include "cpr_stdlib.h"
 
+#include "jsapi.h"
 #include "nspr.h"
 #include "nss.h"
 #include "pk11pub.h"
 
 #include "nsNetCID.h"
+#include "nsIProperty.h"
+#include "nsIPropertyBag2.h"
 #include "nsIServiceManager.h"
+#include "nsISimpleEnumerator.h"
 #include "nsServiceManagerUtils.h"
 #include "nsISocketTransportService.h"
+
 #include "nsThreadUtils.h"
 #include "nsProxyRelease.h"
 
 #include "runnable_utils.h"
 #include "PeerConnectionCtx.h"
 #include "PeerConnectionImpl.h"
 
 #include "nsPIDOMWindow.h"
@@ -725,84 +730,151 @@ PeerConnectionImpl::NotifyDataChannel(mo
       mThread->Dispatch(runnable, NS_DISPATCH_NORMAL);
       return;
     }
     runnable->Run();
   }
 #endif
 }
 
-/*
- * the Constraints UI IDL work is being done. The CreateOffer below is the one
- * currently called by the signaling unit tests.
+/**
+ * Constraints look like this:
+ *
+ * {
+ *    "mandatory": {"foo":"hello", "bar": false, "baz": 10},
+ *    "optional": [{"hello":"foo"}, {"baz": false}]
+ * }
+ *
+ * Optional constraints are ordered, and hence in an array. This function
+ * converts a jsval that looks like the above into a MediaConstraints object.
  */
-NS_IMETHODIMP
-PeerConnectionImpl::CreateOffer(const char* constraints) {
-  MOZ_ASSERT(constraints);
+nsresult
+PeerConnectionImpl::ConvertConstraints(
+  const JS::Value& aConstraints, MediaConstraints* aObj, JSContext* aCx)
+{
+  size_t i;
+  jsval mandatory, optional;
+  JSObject& constraints = aConstraints.toObject();
+
+  // Mandatory constraints.
+  if (JS_GetProperty(aCx, &constraints, "mandatory", &mandatory)) {
+    if (JSVAL_IS_PRIMITIVE(mandatory) && mandatory.isObject() && !JSVAL_IS_NULL(mandatory)) {
+      JSObject* opts = JSVAL_TO_OBJECT(mandatory);
+      JS::AutoIdArray mandatoryOpts(aCx, JS_Enumerate(aCx, opts));
 
-  CheckIceState();
-  mRole = kRoleOfferer;  // TODO(ekr@rtfm.com): Interrogate SIPCC here?
-  MediaConstraints aconstraints;
-  CreateOffer(aconstraints);
+      // Iterate over each property.
+      for (i = 0; i < mandatoryOpts.length(); i++) {
+        jsval option, optionName;
+        if (JS_GetPropertyById(aCx, opts, mandatoryOpts[i], &option)) {
+          if (JS_IdToValue(aCx, mandatoryOpts[i], &optionName)) {
+            // We only support boolean constraints for now.
+            if (JSVAL_IS_BOOLEAN(option)) {
+              JSString* optionNameString = JS_ValueToString(aCx, optionName);
+              NS_ConvertUTF16toUTF8 stringVal(JS_GetStringCharsZ(aCx, optionNameString));
+              aObj->setBooleanConstraint(stringVal.get(), JSVAL_TO_BOOLEAN(option), true);
+            }
+          }
+        }
+      }
+    }
+  }
+
+  // Optional constraints.
+  if (JS_GetProperty(aCx, &constraints, "optional", &optional)) {
+    if (JSVAL_IS_PRIMITIVE(optional) && optional.isObject() && !JSVAL_IS_NULL(optional)) {
+      JSObject* opts = JSVAL_TO_OBJECT(optional);
+      if (JS_IsArrayObject(aCx, opts)) {
+        uint32_t length;
+        if (!JS_GetArrayLength(aCx, opts, &length)) {
+          return NS_ERROR_FAILURE;
+        }
+        for (i = 0; i < length; i++) {
+          jsval val;
+          JS_GetElement(aCx, opts, i, &val);
+          if (JSVAL_IS_PRIMITIVE(val)) {
+            // Extract name & value and store.
+            // FIXME: MediaConstraints does not support optional constraints?
+          }
+        }
+      }
+    }
+  }
+
   return NS_OK;
 }
 
 NS_IMETHODIMP
-PeerConnectionImpl::CreateOffer(MediaConstraints& constraints) {
+PeerConnectionImpl::CreateOffer(const JS::Value& aConstraints, JSContext* aCx)
+{
+  CheckIceState();
+  mRole = kRoleOfferer;  // TODO(ekr@rtfm.com): Interrogate SIPCC here?
 
+  MediaConstraints* cs = new MediaConstraints();
+  nsresult rv = ConvertConstraints(aConstraints, cs, aCx);
+  if (rv != NS_OK) {
+    return rv;
+  }
+
+  return CreateOffer(*cs);
+}
+
+// Used by unit tests and the IDL CreateOffer.
+NS_IMETHODIMP
+PeerConnectionImpl::CreateOffer(MediaConstraints& constraints)
+{
   cc_media_constraints_t* cc_constraints = nullptr;
   constraints.buildArray(&cc_constraints);
 
   mCall->createOffer(cc_constraints);
   return NS_OK;
 }
 
-/*
- * the Constraints UI IDL work is being done. The CreateAnswer below is the one
- * currently called by the signaling unit tests.
- *
- * The aOffer parameter needs to be removed here and in the PeerConnection IDL
- */
-
 NS_IMETHODIMP
-PeerConnectionImpl::CreateAnswer(const char* constraints, const char* aOffer) {
-  MOZ_ASSERT(constraints);
-
+PeerConnectionImpl::CreateAnswer(const JS::Value& aConstraints, JSContext* aCx)
+{
   CheckIceState();
   mRole = kRoleAnswerer;  // TODO(ekr@rtfm.com): Interrogate SIPCC here?
-  MediaConstraints aconstraints;
-  CreateAnswer(aconstraints);
+
+  MediaConstraints* cs = new MediaConstraints();
+  nsresult rv = ConvertConstraints(aConstraints, cs, aCx);
+  if (rv != NS_OK) {
+    return rv;
+  }
+
+  CreateAnswer(*cs);
   return NS_OK;
 }
 
 NS_IMETHODIMP
-PeerConnectionImpl::CreateAnswer(MediaConstraints& constraints) {
-
+PeerConnectionImpl::CreateAnswer(MediaConstraints& constraints)
+{
   cc_media_constraints_t* cc_constraints = nullptr;
   constraints.buildArray(&cc_constraints);
 
   mCall->createAnswer(cc_constraints);
   return NS_OK;
 }
 
 NS_IMETHODIMP
-PeerConnectionImpl::SetLocalDescription(int32_t aAction, const char* aSDP) {
+PeerConnectionImpl::SetLocalDescription(int32_t aAction, const char* aSDP)
+{
   if (!aSDP) {
     CSFLogError(logTag, "%s - aSDP is NULL", __FUNCTION__);
     return NS_ERROR_FAILURE;
   }
 
   CheckIceState();
   mLocalRequestedSDP = aSDP;
   mCall->setLocalDescription((cc_jsep_action_t)aAction, mLocalRequestedSDP);
   return NS_OK;
 }
 
 NS_IMETHODIMP
-PeerConnectionImpl::SetRemoteDescription(int32_t action, const char* aSDP) {
+PeerConnectionImpl::SetRemoteDescription(int32_t action, const char* aSDP)
+{
   if (!aSDP) {
     CSFLogError(logTag, "%s - aSDP is NULL", __FUNCTION__);
     return NS_ERROR_FAILURE;
   }
 
   CheckIceState();
   mRemoteRequestedSDP = aSDP;
   mCall->setRemoteDescription((cc_jsep_action_t)action, mRemoteRequestedSDP);
--- a/media/webrtc/signaling/src/peerconnection/PeerConnectionImpl.h
+++ b/media/webrtc/signaling/src/peerconnection/PeerConnectionImpl.h
@@ -391,19 +391,22 @@ public:
   // Get the DTLS identity
   mozilla::RefPtr<DtlsIdentity> const GetIdentity() { return mIdentity; }
 
   // Create a fake media stream
   nsresult CreateFakeMediaStream(uint32_t hint, nsIDOMMediaStream** retval);
 
   nsPIDOMWindow* GetWindow() const { return mWindow; }
 
-  NS_IMETHODIMP CreateOffer(MediaConstraints& constraints);
-
-  NS_IMETHODIMP CreateAnswer(MediaConstraints& constraints);
+  // Validate constraints and construct a MediaConstraints object
+  // from a JS::Value.
+  nsresult ConvertConstraints(
+    const JS::Value& aConstraints, MediaConstraints* aObj, JSContext* aCx);
+  NS_IMETHODIMP CreateOffer(MediaConstraints& aConstraints);
+  NS_IMETHODIMP CreateAnswer(MediaConstraints& aConstraints);
 
 private:
   PeerConnectionImpl(const PeerConnectionImpl&rhs);
   PeerConnectionImpl& operator=(PeerConnectionImpl);
 
   void ChangeReadyState(ReadyState aReadyState);
   void CheckIceState() {
     PR_ASSERT(mIceState != kIceGathering);