Bug 1473467: implement AudioWorkletGlobalScope::RegisterProcessor(). r=baku,karlt

Differential Revision: https://phabricator.services.mozilla.com/D6368

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Arnaud Bienner 2018-10-08 19:15:13 +00:00
Родитель 81a039f147
Коммит bb6b29da9d
9 изменённых файлов: 361 добавлений и 10 удалений

Просмотреть файл

@ -98,6 +98,10 @@ DOMInterfaces = {
'nativeType': 'mozilla::dom::Worklet', 'nativeType': 'mozilla::dom::Worklet',
}, },
'AudioWorkletGlobalScope': {
'implicitJSContext': [ 'registerProcessor' ],
},
'BarProp': { 'BarProp': {
'headerFile': 'mozilla/dom/BarProps.h', 'headerFile': 'mozilla/dom/BarProps.h',
}, },

Просмотреть файл

@ -115,3 +115,4 @@ MSG_DEF(MSG_VALUE_OUT_OF_RANGE, 1, JSEXN_RANGEERR, "The value for the {0} is out
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_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_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_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.")

Просмотреть файл

@ -9,6 +9,7 @@
[Global=(Worklet,AudioWorklet),Exposed=AudioWorklet] [Global=(Worklet,AudioWorklet),Exposed=AudioWorklet]
interface AudioWorkletGlobalScope : WorkletGlobalScope { interface AudioWorkletGlobalScope : WorkletGlobalScope {
[Throws]
void registerProcessor (DOMString name, VoidFunction processorCtor); void registerProcessor (DOMString name, VoidFunction processorCtor);
readonly attribute unsigned long long currentFrame; readonly attribute unsigned long long currentFrame;
readonly attribute double currentTime; readonly attribute double currentTime;

Просмотреть файл

@ -5,13 +5,22 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "AudioWorkletGlobalScope.h" #include "AudioWorkletGlobalScope.h"
#include "WorkletPrincipal.h" #include "jsapi.h"
#include "mozilla/dom/AudioWorkletGlobalScopeBinding.h" #include "mozilla/dom/AudioWorkletGlobalScopeBinding.h"
#include "mozilla/dom/FunctionBinding.h" #include "WorkletPrincipal.h"
namespace mozilla { namespace mozilla {
namespace dom { 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) AudioWorkletGlobalScope::AudioWorkletGlobalScope(WorkletImpl* aImpl)
: WorkletGlobalScope(aImpl) : WorkletGlobalScope(aImpl)
, mCurrentFrame(0) , mCurrentFrame(0)
@ -31,10 +40,153 @@ AudioWorkletGlobalScope::WrapGlobalObject(JSContext* aCx,
} }
void void
AudioWorkletGlobalScope::RegisterProcessor(const nsAString& aType, AudioWorkletGlobalScope::RegisterProcessor(JSContext* aCx,
VoidFunction& aProcessorCtor) 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 uint64_t AudioWorkletGlobalScope::CurrentFrame() const

Просмотреть файл

@ -7,7 +7,9 @@
#ifndef mozilla_dom_AudioWorkletGlobalScope_h #ifndef mozilla_dom_AudioWorkletGlobalScope_h
#define mozilla_dom_AudioWorkletGlobalScope_h #define mozilla_dom_AudioWorkletGlobalScope_h
#include "mozilla/dom/FunctionBinding.h"
#include "mozilla/dom/WorkletGlobalScope.h" #include "mozilla/dom/WorkletGlobalScope.h"
#include "nsRefPtrHashtable.h"
namespace mozilla { namespace mozilla {
@ -15,11 +17,13 @@ class WorkletImpl;
namespace dom { namespace dom {
class VoidFunction;
class AudioWorkletGlobalScope final : public WorkletGlobalScope class AudioWorkletGlobalScope final : public WorkletGlobalScope
{ {
public: public:
NS_DECL_ISUPPORTS_INHERITED
NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(AudioWorkletGlobalScope, WorkletGlobalScope);
explicit AudioWorkletGlobalScope(WorkletImpl* aImpl); explicit AudioWorkletGlobalScope(WorkletImpl* aImpl);
bool bool
@ -27,8 +31,9 @@ public:
JS::MutableHandle<JSObject*> aReflector) override; JS::MutableHandle<JSObject*> aReflector) override;
void void
RegisterProcessor(const nsAString& aType, RegisterProcessor(JSContext* aCx, const nsAString& aName,
VoidFunction& aProcessorCtor); VoidFunction& aProcessorCtor,
ErrorResult& aRv);
uint64_t CurrentFrame() const; uint64_t CurrentFrame() const;
@ -42,6 +47,9 @@ private:
uint64_t mCurrentFrame; uint64_t mCurrentFrame;
double mCurrentTime; double mCurrentTime;
float mSampleRate; float mSampleRate;
typedef nsRefPtrHashtable<nsStringHashKey, VoidFunction> NodeNameToProcessorDefinitionMap;
NodeNameToProcessorDefinitionMap mNameToProcessorMap;
}; };
} // namespace dom } // namespace dom

Просмотреть файл

@ -16,6 +16,8 @@ support-files=worklet_dump.js
scheme = http scheme = http
[test_audioWorklet.html] [test_audioWorklet.html]
support-files=worklet_audioWorklet.js support-files=worklet_audioWorklet.js
[test_audioWorkletGlobalScopeRegisterProcessor.html]
support-files=worklet_test_audioWorkletGlobalScopeRegisterProcessor.js
[test_exception.html] [test_exception.html]
support-files=worklet_exception.js support-files=worklet_exception.js
[test_paintWorklet.html] [test_paintWorklet.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>

Просмотреть файл

@ -1,3 +1,12 @@
// This should work for real... at some point. class DummyProcessWorkletProcessor extends AudioWorkletProcessor {
registerProcessor("sure!", () => {}); 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"); console.log(this instanceof AudioWorkletGlobalScope ? "So far so good" : "error");

Просмотреть файл

@ -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)
}