Bug 1473467: implement AudioWorkletGlobalScope::RegisterProcessor(). r=baku,karlt
authorArnaud Bienner <arnaud.bienner@gmail.com>
Mon, 08 Oct 2018 19:15:13 +0000
changeset 495747 7355fb2908e5087eddab422ac838697284c25d82
parent 495746 7de7e0e7ed1ac3feeb34544a3fd5c891ee47c137
child 495748 3ded0ccdc4a548190faf633dca587cd11744b6f4
push id9984
push userffxbld-merge
push dateMon, 15 Oct 2018 21:07:35 +0000
treeherdermozilla-beta@183d27ea8570 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbaku, karlt
bugs1473467
milestone64.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 1473467: implement AudioWorkletGlobalScope::RegisterProcessor(). r=baku,karlt Differential Revision: https://phabricator.services.mozilla.com/D6368
dom/bindings/Bindings.conf
dom/bindings/Errors.msg
dom/webidl/AudioWorkletGlobalScope.webidl
dom/worklet/AudioWorkletGlobalScope.cpp
dom/worklet/AudioWorkletGlobalScope.h
dom/worklet/tests/mochitest.ini
dom/worklet/tests/test_audioWorkletGlobalScopeRegisterProcessor.html
dom/worklet/tests/worklet_audioWorklet.js
dom/worklet/tests/worklet_test_audioWorkletGlobalScopeRegisterProcessor.js
--- a/dom/bindings/Bindings.conf
+++ b/dom/bindings/Bindings.conf
@@ -93,16 +93,20 @@ DOMInterfaces = {
         'channelInterpretation': 'channelInterpretationValue',
     },
 },
 
 'AudioWorklet': {
     'nativeType': 'mozilla::dom::Worklet',
 },
 
+'AudioWorkletGlobalScope': {
+    'implicitJSContext': [ 'registerProcessor' ],
+},
+
 'BarProp': {
     'headerFile': 'mozilla/dom/BarProps.h',
 },
 
 'BaseAudioContext': {
     'nativeType': 'mozilla::dom::AudioContext',
 },
 
--- a/dom/bindings/Errors.msg
+++ b/dom/bindings/Errors.msg
@@ -110,8 +110,9 @@ MSG_DEF(MSG_INVALID_CURVE_DURATION_ERROR
 MSG_DEF(MSG_INVALID_AUDIOPARAM_METHOD_START_TIME_ERROR, 0, JSEXN_RANGEERR, "The start time for an AudioParam method must be non-negative.")
 MSG_DEF(MSG_INVALID_AUDIOPARAM_METHOD_END_TIME_ERROR, 0, JSEXN_RANGEERR, "The end time for an AudioParam method must be non-negative.")
 MSG_DEF(MSG_INVALID_AUDIOPARAM_EXPONENTIAL_VALUE_ERROR, 0, JSEXN_RANGEERR, "The value passed to exponentialRampToValueAtTime must be positive.")
 MSG_DEF(MSG_INVALID_AUDIOPARAM_EXPONENTIAL_CONSTANT_ERROR, 0, JSEXN_RANGEERR, "The exponential constant passed to setTargetAtTime must be non-negative.")
 MSG_DEF(MSG_VALUE_OUT_OF_RANGE, 1, JSEXN_RANGEERR, "The value for the {0} is outside the valid range.")
 MSG_DEF(MSG_INVALID_PANNERNODE_REFDISTANCE_ERROR, 0, JSEXN_RANGEERR, "The refDistance value passed to PannerNode must not be negative.")
 MSG_DEF(MSG_INVALID_PANNERNODE_MAXDISTANCE_ERROR, 0, JSEXN_RANGEERR, "The maxDistance value passed to PannerNode must be positive.")
 MSG_DEF(MSG_INVALID_PANNERNODE_ROLLOFF_ERROR, 0, JSEXN_RANGEERR, "The rolloffFactor value passed to PannerNode must not be negative.")
+MSG_DEF(MSG_NOT_ARRAY_NOR_UNDEFINED, 1, JSEXN_TYPEERR, "{0} is neither an array nor undefined.")
--- a/dom/webidl/AudioWorkletGlobalScope.webidl
+++ b/dom/webidl/AudioWorkletGlobalScope.webidl
@@ -4,13 +4,14 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/.
  *
  * The origin of this IDL file is
  * https://webaudio.github.io/web-audio-api/#audioworkletglobalscope
  */
 
 [Global=(Worklet,AudioWorklet),Exposed=AudioWorklet]
 interface AudioWorkletGlobalScope : WorkletGlobalScope {
+    [Throws]
     void registerProcessor (DOMString name, VoidFunction processorCtor);
     readonly  attribute   unsigned long long currentFrame;
     readonly  attribute   double currentTime;
     readonly  attribute   float sampleRate;
 };
--- a/dom/worklet/AudioWorkletGlobalScope.cpp
+++ b/dom/worklet/AudioWorkletGlobalScope.cpp
@@ -1,22 +1,31 @@
 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
 /* 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 "AudioWorkletGlobalScope.h"
-#include "WorkletPrincipal.h"
+#include "jsapi.h"
 #include "mozilla/dom/AudioWorkletGlobalScopeBinding.h"
-#include "mozilla/dom/FunctionBinding.h"
+#include "WorkletPrincipal.h"
 
 namespace mozilla {
 namespace dom {
 
+NS_IMPL_CYCLE_COLLECTION_INHERITED(AudioWorkletGlobalScope, WorkletGlobalScope,
+                                   mNameToProcessorMap);
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(AudioWorkletGlobalScope)
+NS_INTERFACE_MAP_END_INHERITING(WorkletGlobalScope)
+
+NS_IMPL_ADDREF_INHERITED(AudioWorkletGlobalScope, WorkletGlobalScope)
+NS_IMPL_RELEASE_INHERITED(AudioWorkletGlobalScope, WorkletGlobalScope)
+
 AudioWorkletGlobalScope::AudioWorkletGlobalScope(WorkletImpl* aImpl)
   : WorkletGlobalScope(aImpl)
   , mCurrentFrame(0)
   , mCurrentTime(0)
   , mSampleRate(0.0)
 {}
 
 bool
@@ -26,20 +35,163 @@ AudioWorkletGlobalScope::WrapGlobalObjec
   JS::RealmOptions options;
   return AudioWorkletGlobalScope_Binding::Wrap(aCx, this, this,
                                               options,
                                               WorkletPrincipal::GetWorkletPrincipal(),
                                               true, aReflector);
 }
 
 void
-AudioWorkletGlobalScope::RegisterProcessor(const nsAString& aType,
-                                           VoidFunction& aProcessorCtor)
+AudioWorkletGlobalScope::RegisterProcessor(JSContext* aCx,
+                                           const nsAString& aName,
+                                           VoidFunction& aProcessorCtor,
+                                           ErrorResult& aRv)
 {
-  // Nothing to do here.
+  JS::Rooted<JSObject*> processorConstructor(aCx, aProcessorCtor.CallableOrNull());
+
+  /**
+   * 1. If the name is the empty string, throw a NotSupportedError
+   *    exception and abort these steps because the empty string is not
+   *    a valid key.
+   */
+  if (aName.IsEmpty()) {
+    aRv.ThrowDOMException(NS_ERROR_DOM_NOT_SUPPORTED_ERR,
+            NS_LITERAL_CSTRING(
+                "Argument 1 of AudioWorkletGlobalScope.registerProcessor "
+                "should not be an empty string."));
+    return;
+  }
+
+  /**
+   * 2. If the name exists as a key in the node name to processor
+   *    definition map, throw a NotSupportedError exception and abort
+   *    these steps because registering a definition with a duplicated
+   *    key is not allowed.
+   */
+  if (mNameToProcessorMap.GetWeak(aName)) {
+    // Duplicate names are not allowed
+    aRv.ThrowDOMException(NS_ERROR_DOM_NOT_SUPPORTED_ERR,
+        NS_LITERAL_CSTRING(
+            "Argument 1 of AudioWorkletGlobalScope.registerProcessor "
+            "is invalid: a class with the same name is already registered."));
+    return;
+  }
+
+  JS::Rooted<JSObject*> constructorUnwrapped(
+      aCx, js::CheckedUnwrap(processorConstructor));
+  if (!constructorUnwrapped) {
+    // If the caller's compartment does not have permission to access the
+    // unwrapped constructor then throw.
+    aRv.Throw(NS_ERROR_DOM_SECURITY_ERR);
+    return;
+  }
+
+  /**
+   * 3. If the result of IsConstructor(argument=processorCtor) is false,
+   *    throw a TypeError and abort these steps.
+   */
+  if (!JS::IsConstructor(constructorUnwrapped)) {
+    aRv.ThrowTypeError<MSG_NOT_CONSTRUCTOR>(NS_LITERAL_STRING(
+        "Argument 2 of AudioWorkletGlobalScope.registerProcessor"));
+    return;
+  }
+
+  /**
+   * 4. Let prototype be the result of Get(O=processorCtor, P="prototype").
+   */
+  // The .prototype on the constructor passed could be an "expando" of a
+  // wrapper. So we should get it from wrapper instead of the underlying
+  // object.
+  JS::Rooted<JS::Value> prototype(aCx);
+  if (!JS_GetProperty(aCx, processorConstructor, "prototype", &prototype)) {
+    aRv.NoteJSContextException(aCx);
+    return;
+  }
+
+  /**
+   * 5. If the result of Type(argument=prototype) is not Object, throw a
+   *    TypeError and abort all these steps.
+   */
+  if (!prototype.isObject()) {
+    aRv.ThrowTypeError<MSG_NOT_OBJECT>(NS_LITERAL_STRING(
+        "Argument 2 of AudioWorkletGlobalScope.registerProcessor "
+        "processorCtor.prototype"));
+    return;
+  }
+
+  /**
+   * 6. If the result of IsCallable(argument=Get(O=prototype, P="process"))
+   *    is false, throw a TypeError and abort these steps.
+   */
+  JS::Rooted<JS::Value> process(aCx);
+  JS::Rooted<JSObject*> prototypeObject(aCx, &prototype.toObject());
+  if (!JS_GetProperty(aCx, prototypeObject, "process", &process)) {
+    aRv.NoteJSContextException(aCx);
+    return;
+  }
+
+  if (!process.isObjectOrNull() ||
+      !JS::IsCallable(process.toObjectOrNull())) {
+    aRv.ThrowTypeError<MSG_NOT_CALLABLE>(NS_LITERAL_STRING(
+        "Argument 2 of AudioWorkletGlobalScope.registerProcessor "
+        "constructor.process"));
+    return;
+  }
+
+  /**
+   * 7. Let descriptors be the result of Get(O=processorCtor,
+   *    P="parameterDescriptors").
+   */
+  JS::Rooted<JS::Value> descriptors(aCx);
+  if (!JS_GetProperty(aCx, processorConstructor, "parameterDescriptors",
+                      &descriptors)) {
+    aRv.NoteJSContextException(aCx);
+    return;
+  }
+
+  /**
+   * 8. If descriptors is neither an array nor undefined, throw a
+   *    TypeError and abort these steps.
+   */
+  bool isArray = false;
+  if (!JS_IsArrayObject(aCx, descriptors, &isArray)) {
+    // I would assume isArray won't be set to true if JS_IsArrayObject
+    // failed, but just in case, force it to false
+    isArray = false;
+    JS_ClearPendingException(aCx);
+  }
+
+  if (!descriptors.isUndefined() && !isArray) {
+    aRv.ThrowTypeError<MSG_NOT_ARRAY_NOR_UNDEFINED>(NS_LITERAL_STRING(
+           "Argument 2 of AudioWorkletGlobalScope.registerProcessor "
+           "constructor.parameterDescriptors"));
+    return;
+  }
+
+  /**
+   * 9. Let definition be a new AudioWorkletProcessor definition with:
+   *    - node name being name
+   *    - processor class constructor being processorCtor
+   * 10. Add the key-value pair (name - definition) to the node name to
+   *     processor definition map of the associated AudioWorkletGlobalScope.
+   */
+  if (!mNameToProcessorMap.Put(aName, &aProcessorCtor, fallible)) {
+    aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
+    return;
+  }
+
+  /**
+   * 11. Queue a task to the control thread to add the key-value pair
+   *     (name - descriptors) to the node name to parameter descriptor
+   *     map of the associated BaseAudioContext.
+   */
+  // TODO: we don't have a proper mechanism to communicate with the
+  // control thread currently. See
+  // https://bugzilla.mozilla.org/show_bug.cgi?id=1473467#c3
+  // and https://bugzilla.mozilla.org/show_bug.cgi?id=1492014
 }
 
 uint64_t AudioWorkletGlobalScope::CurrentFrame() const
 {
   return mCurrentFrame;
 }
 
 double AudioWorkletGlobalScope::CurrentTime() const
--- a/dom/worklet/AudioWorkletGlobalScope.h
+++ b/dom/worklet/AudioWorkletGlobalScope.h
@@ -2,49 +2,57 @@
 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
 /* 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/. */
 
 #ifndef mozilla_dom_AudioWorkletGlobalScope_h
 #define mozilla_dom_AudioWorkletGlobalScope_h
 
+#include "mozilla/dom/FunctionBinding.h"
 #include "mozilla/dom/WorkletGlobalScope.h"
+#include "nsRefPtrHashtable.h"
 
 namespace mozilla {
 
 class WorkletImpl;
 
 namespace dom {
 
-class VoidFunction;
 
 class AudioWorkletGlobalScope final : public WorkletGlobalScope
 {
 public:
+  NS_DECL_ISUPPORTS_INHERITED
+  NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(AudioWorkletGlobalScope, WorkletGlobalScope);
+
   explicit AudioWorkletGlobalScope(WorkletImpl* aImpl);
 
   bool
   WrapGlobalObject(JSContext* aCx,
                    JS::MutableHandle<JSObject*> aReflector) override;
 
   void
-  RegisterProcessor(const nsAString& aType,
-                    VoidFunction& aProcessorCtor);
+  RegisterProcessor(JSContext* aCx, const nsAString& aName,
+                    VoidFunction& aProcessorCtor,
+                    ErrorResult& aRv);
 
   uint64_t CurrentFrame() const;
 
   double CurrentTime() const;
 
   float SampleRate() const;
 
 private:
   ~AudioWorkletGlobalScope() = default;
 
   uint64_t mCurrentFrame;
   double mCurrentTime;
   float mSampleRate;
+
+  typedef nsRefPtrHashtable<nsStringHashKey, VoidFunction> NodeNameToProcessorDefinitionMap;
+  NodeNameToProcessorDefinitionMap mNameToProcessorMap;
 };
 
 } // namespace dom
 } // namespace mozilla
 
 #endif // mozilla_dom_AudioWorkletGlobalScope_h
--- a/dom/worklet/tests/mochitest.ini
+++ b/dom/worklet/tests/mochitest.ini
@@ -11,13 +11,15 @@ support-files=worklet_console.js
 skip-if = verify
 support-files=server_import_with_cache.sjs
 [test_dump.html]
 support-files=worklet_dump.js
 [test_audioWorklet_insecureContext.html]
 scheme = http
 [test_audioWorklet.html]
 support-files=worklet_audioWorklet.js
+[test_audioWorkletGlobalScopeRegisterProcessor.html]
+support-files=worklet_test_audioWorkletGlobalScopeRegisterProcessor.js
 [test_exception.html]
 support-files=worklet_exception.js
 [test_paintWorklet.html]
 scheme = http
 support-files=worklet_paintWorklet.js
new file mode 100644
--- /dev/null
+++ b/dom/worklet/tests/test_audioWorkletGlobalScopeRegisterProcessor.html
@@ -0,0 +1,68 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <title>Test for AudioWorklet</title>
+  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+  <script type="application/javascript" src="common.js"></script>
+</head>
+<body>
+
+<script type="application/javascript">
+
+function configureTest() {
+
+  var expected_errors = [
+    "TypeError: Argument 2 of AudioWorkletGlobalScope.registerProcessor is not a constructor.",
+    "NotSupportedError: Argument 1 of AudioWorkletGlobalScope.registerProcessor should not be an empty string.",
+    "TypeError: Argument 2 of AudioWorkletGlobalScope.registerProcessor is not an object.",
+    "TypeError: Argument 2 of AudioWorkletGlobalScope.registerProcessor constructor.process is not callable.",
+    "TypeError: Argument 2 of AudioWorkletGlobalScope.registerProcessor constructor.process is not callable.",
+    "TypeError: Argument 2 of AudioWorkletGlobalScope.registerProcessor constructor.parameterDescriptors is neither an array nor undefined.",
+    "NotSupportedError: Argument 1 of AudioWorkletGlobalScope.registerProcessor is invalid: a class with the same name is already registered.",
+    ];
+
+  var expected_errors_i = 0;
+
+  function consoleListener() {
+    SpecialPowers.addObserver(this, "console-api-log-event");
+  }
+
+  consoleListener.prototype  = {
+    observe: function(aSubject, aTopic, aData) {
+      if (aTopic == "console-api-log-event") {
+        var obj = aSubject.wrappedJSObject;
+        if (obj.arguments[0] == expected_errors[expected_errors_i]) {
+          ok(true, "Expected error received: " + obj.arguments[0]);
+          expected_errors_i++;
+        }
+
+        if (expected_errors_i == expected_errors.length) {
+          // All errors have been received, this test has been completed
+          // succesfully!
+          SpecialPowers.removeObserver(this, "console-api-log-event");
+          SimpleTest.finish();
+          return;
+        }
+      }
+    }
+  }
+
+  var cl = new consoleListener();
+
+  return SpecialPowers.pushPrefEnv(
+    {"set": [["dom.audioworklet.enabled", true],
+             ["dom.worklet.enabled", true]]});
+}
+
+// This function is called into an iframe.
+function runTestInIframe() {
+  ok(window.isSecureContext, "Test should run in secure context");
+  var audioContext = new AudioContext();
+  ok(audioContext.audioWorklet instanceof AudioWorklet,
+     "AudioContext.audioWorklet should be an instance of AudioWorklet");
+  audioContext.audioWorklet.addModule("worklet_test_audioWorkletGlobalScopeRegisterProcessor.js")
+}
+</script>
+</body>
+</html>
--- a/dom/worklet/tests/worklet_audioWorklet.js
+++ b/dom/worklet/tests/worklet_audioWorklet.js
@@ -1,3 +1,12 @@
-// This should work for real... at some point.
-registerProcessor("sure!", () => {});
+class DummyProcessWorkletProcessor extends AudioWorkletProcessor {
+  constructor() { super(); }
+
+  process() {
+    // Do nothing, output silence
+  }
+}
+
+// We need to pass a valid AudioWorkletProcessor here, otherwise, it will fail,
+// and the console.log won't be executed
+registerProcessor("sure!", DummyProcessWorkletProcessor);
 console.log(this instanceof AudioWorkletGlobalScope ? "So far so good" : "error");
new file mode 100644
--- /dev/null
+++ b/dom/worklet/tests/worklet_test_audioWorkletGlobalScopeRegisterProcessor.js
@@ -0,0 +1,106 @@
+// Define several classes.
+// Only the last ones are valid.
+class EmptyWorkletProcessor extends AudioWorkletProcessor {
+}
+
+class NoProcessWorkletProcessor extends AudioWorkletProcessor {
+  constructor() { super(); }
+}
+
+class BadDescriptorsWorkletProcessor extends AudioWorkletProcessor {
+  constructor() { super(); }
+
+  process() {
+    // Do nothing, output silence
+  }
+
+  static get parameterDescriptors() {
+    return "A string";
+  }
+}
+
+class GoodDescriptorsWorkletProcessor extends AudioWorkletProcessor {
+  constructor() { super(); }
+
+  process() {
+    // Do nothing, output silence
+  }
+
+  static get parameterDescriptors() {
+    return [{
+      name: 'myParam', defaultValue: 0.707
+    }];
+  }
+}
+
+class DummyProcessWorkletProcessor extends AudioWorkletProcessor {
+  constructor() { super(); }
+
+  process() {
+    // Do nothing, output silence
+  }
+}
+
+// Test not a constructor
+// "TypeError: Argument 2 of AudioWorkletGlobalScope.registerProcessor is not a constructor."
+try {
+  registerProcessor("sure!", () => {});
+} catch (e) {
+  console.log(e)
+}
+
+// Test empty name
+// "NotSupportedError: Argument 1 of AudioWorkletGlobalScope.registerProcessor should not be an empty string."
+try {
+  registerProcessor("", EmptyWorkletProcessor);
+} catch (e) {
+  console.log(e)
+}
+
+// Test not an object
+// "TypeError: Argument 2 of AudioWorkletGlobalScope.registerProcessor is not an object."
+try {
+  registerProcessor("my-worklet-processor", "");
+} catch (e) {
+  console.log(e)
+}
+
+// Test Empty class definition
+// "TypeError: Argument 2 of AudioWorkletGlobalScope.registerProcessor constructor.process is not callable."
+try {
+  registerProcessor("empty-worklet-processor", EmptyWorkletProcessor);
+} catch (e) {
+  console.log(e)
+}
+
+// Test class with constructor but not process function
+// "TypeError: Argument 2 of AudioWorkletGlobalScope.registerProcessor constructor.process is not callable."
+try {
+  registerProcessor("no-worklet-processor", NoProcessWorkletProcessor);
+} catch (e) {
+  console.log(e)
+}
+
+// Test class with parameterDescriptors not being array nor undefined
+// "TypeError: Argument 2 of AudioWorkletGlobalScope.registerProcessor constructor.parameterDescriptors is neither an array nor undefined."
+try {
+  registerProcessor("bad-descriptors-worklet-processor", BadDescriptorsWorkletProcessor);
+} catch (e) {
+  console.log(e)
+}
+
+// Test class with good parameterDescriptors
+// No error expected here
+registerProcessor("good-descriptors-worklet-processor", GoodDescriptorsWorkletProcessor);
+
+// Test class with constructor and process function
+// No error expected here
+registerProcessor("dummy-worklet-processor", DummyProcessWorkletProcessor);
+
+// Test class adding class with the same name twice
+// "NotSupportedError: Operation is not supported: Argument 1 of AudioWorkletGlobalScope.registerProcessor is invalid: a class with the same name is already registered."
+try {
+  registerProcessor("dummy-worklet-processor", DummyProcessWorkletProcessor);
+} catch (e) {
+  console.log(e)
+}