Wed, 31 Dec 2014 06:09:35 +0100
Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.
1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4 "use strict";
6 const {Cc, Ci, Cu, Cr} = require("chrome");
8 const Services = require("Services");
10 const { Promise: promise } = Cu.import("resource://gre/modules/Promise.jsm", {});
11 const events = require("sdk/event/core");
12 const protocol = require("devtools/server/protocol");
13 const { CallWatcherActor, CallWatcherFront } = require("devtools/server/actors/call-watcher");
15 const { on, once, off, emit } = events;
16 const { method, Arg, Option, RetVal } = protocol;
18 exports.register = function(handle) {
19 handle.addTabActor(WebAudioActor, "webaudioActor");
20 };
22 exports.unregister = function(handle) {
23 handle.removeTabActor(WebAudioActor);
24 };
26 const AUDIO_GLOBALS = [
27 "AudioContext", "AudioNode"
28 ];
30 const NODE_CREATION_METHODS = [
31 "createBufferSource", "createMediaElementSource", "createMediaStreamSource",
32 "createMediaStreamDestination", "createScriptProcessor", "createAnalyser",
33 "createGain", "createDelay", "createBiquadFilter", "createWaveShaper",
34 "createPanner", "createConvolver", "createChannelSplitter", "createChannelMerger",
35 "createDynamicsCompressor", "createOscillator"
36 ];
38 const NODE_ROUTING_METHODS = [
39 "connect", "disconnect"
40 ];
42 const NODE_PROPERTIES = {
43 "OscillatorNode": {
44 "type": {},
45 "frequency": {},
46 "detune": {}
47 },
48 "GainNode": {
49 "gain": {}
50 },
51 "DelayNode": {
52 "delayTime": {}
53 },
54 "AudioBufferSourceNode": {
55 "buffer": { "Buffer": true },
56 "playbackRate": {},
57 "loop": {},
58 "loopStart": {},
59 "loopEnd": {}
60 },
61 "ScriptProcessorNode": {
62 "bufferSize": { "readonly": true }
63 },
64 "PannerNode": {
65 "panningModel": {},
66 "distanceModel": {},
67 "refDistance": {},
68 "maxDistance": {},
69 "rolloffFactor": {},
70 "coneInnerAngle": {},
71 "coneOuterAngle": {},
72 "coneOuterGain": {}
73 },
74 "ConvolverNode": {
75 "buffer": { "Buffer": true },
76 "normalize": {},
77 },
78 "DynamicsCompressorNode": {
79 "threshold": {},
80 "knee": {},
81 "ratio": {},
82 "reduction": {},
83 "attack": {},
84 "release": {}
85 },
86 "BiquadFilterNode": {
87 "type": {},
88 "frequency": {},
89 "Q": {},
90 "detune": {},
91 "gain": {}
92 },
93 "WaveShaperNode": {
94 "curve": { "Float32Array": true },
95 "oversample": {}
96 },
97 "AnalyserNode": {
98 "fftSize": {},
99 "minDecibels": {},
100 "maxDecibels": {},
101 "smoothingTimeConstraint": {},
102 "frequencyBinCount": { "readonly": true },
103 },
104 "AudioDestinationNode": {},
105 "ChannelSplitterNode": {},
106 "ChannelMergerNode": {}
107 };
109 /**
110 * Track an array of audio nodes
112 /**
113 * An Audio Node actor allowing communication to a specific audio node in the
114 * Audio Context graph.
115 */
116 let AudioNodeActor = exports.AudioNodeActor = protocol.ActorClass({
117 typeName: "audionode",
119 /**
120 * Create the Audio Node actor.
121 *
122 * @param DebuggerServerConnection conn
123 * The server connection.
124 * @param AudioNode node
125 * The AudioNode that was created.
126 */
127 initialize: function (conn, node) {
128 protocol.Actor.prototype.initialize.call(this, conn);
129 this.node = unwrap(node);
130 try {
131 this.type = this.node.toString().match(/\[object (.*)\]$/)[1];
132 } catch (e) {
133 this.type = "";
134 }
135 },
137 /**
138 * Returns the name of the audio type.
139 * Examples: "OscillatorNode", "MediaElementAudioSourceNode"
140 */
141 getType: method(function () {
142 return this.type;
143 }, {
144 response: { type: RetVal("string") }
145 }),
147 /**
148 * Returns a boolean indicating if the node is a source node,
149 * like BufferSourceNode, MediaElementAudioSourceNode, OscillatorNode, etc.
150 */
151 isSource: method(function () {
152 return !!~this.type.indexOf("Source") || this.type === "OscillatorNode";
153 }, {
154 response: { source: RetVal("boolean") }
155 }),
157 /**
158 * Changes a param on the audio node. Responds with a `string` that's either
159 * an empty string `""` on success, or a description of the error upon
160 * param set failure.
161 *
162 * @param String param
163 * Name of the AudioParam to change.
164 * @param String value
165 * Value to change AudioParam to.
166 */
167 setParam: method(function (param, value) {
168 // Strip quotes because sometimes UIs include that for strings
169 if (typeof value === "string") {
170 value = value.replace(/[\'\"]*/g, "");
171 }
172 try {
173 if (isAudioParam(this.node, param))
174 this.node[param].value = value;
175 else
176 this.node[param] = value;
177 return undefined;
178 } catch (e) {
179 return constructError(e);
180 }
181 }, {
182 request: {
183 param: Arg(0, "string"),
184 value: Arg(1, "nullable:primitive")
185 },
186 response: { error: RetVal("nullable:json") }
187 }),
189 /**
190 * Gets a param on the audio node.
191 *
192 * @param String param
193 * Name of the AudioParam to fetch.
194 */
195 getParam: method(function (param) {
196 // If property does not exist, just return "undefined"
197 if (!this.node[param])
198 return undefined;
199 let value = isAudioParam(this.node, param) ? this.node[param].value : this.node[param];
200 return value;
201 }, {
202 request: {
203 param: Arg(0, "string")
204 },
205 response: { text: RetVal("nullable:primitive") }
206 }),
208 /**
209 * Get an object containing key-value pairs of additional attributes
210 * to be consumed by a front end, like if a property should be read only,
211 * or is a special type (Float32Array, Buffer, etc.)
212 *
213 * @param String param
214 * Name of the AudioParam whose flags are desired.
215 */
216 getParamFlags: method(function (param) {
217 return (NODE_PROPERTIES[this.type] || {})[param];
218 }, {
219 request: { param: Arg(0, "string") },
220 response: { flags: RetVal("nullable:primitive") }
221 }),
223 /**
224 * Get an array of objects each containing a `param` and `value` property,
225 * corresponding to a property name and current value of the audio node.
226 */
227 getParams: method(function (param) {
228 let props = Object.keys(NODE_PROPERTIES[this.type]);
229 return props.map(prop =>
230 ({ param: prop, value: this.getParam(prop), flags: this.getParamFlags(prop) }));
231 }, {
232 response: { params: RetVal("json") }
233 })
234 });
236 /**
237 * The corresponding Front object for the AudioNodeActor.
238 */
239 let AudioNodeFront = protocol.FrontClass(AudioNodeActor, {
240 initialize: function (client, form) {
241 protocol.Front.prototype.initialize.call(this, client, form);
242 client.addActorPool(this);
243 this.manage(this);
244 }
245 });
247 /**
248 * The Web Audio Actor handles simple interaction with an AudioContext
249 * high-level methods. After instantiating this actor, you'll need to set it
250 * up by calling setup().
251 */
252 let WebAudioActor = exports.WebAudioActor = protocol.ActorClass({
253 typeName: "webaudio",
254 initialize: function(conn, tabActor) {
255 protocol.Actor.prototype.initialize.call(this, conn);
256 this.tabActor = tabActor;
257 this._onContentFunctionCall = this._onContentFunctionCall.bind(this);
258 },
260 destroy: function(conn) {
261 protocol.Actor.prototype.destroy.call(this, conn);
262 this.finalize();
263 },
265 /**
266 * Starts waiting for the current tab actor's document global to be
267 * created, in order to instrument the Canvas context and become
268 * aware of everything the content does with Web Audio.
269 *
270 * See ContentObserver and WebAudioInstrumenter for more details.
271 */
272 setup: method(function({ reload }) {
273 if (this._initialized) {
274 return;
275 }
276 this._initialized = true;
278 // Weak map mapping audio nodes to their corresponding actors
279 this._nodeActors = new Map();
281 this._callWatcher = new CallWatcherActor(this.conn, this.tabActor);
282 this._callWatcher.onCall = this._onContentFunctionCall;
283 this._callWatcher.setup({
284 tracedGlobals: AUDIO_GLOBALS,
285 startRecording: true,
286 performReload: reload
287 });
289 // Used to track when something is happening with the web audio API
290 // the first time, to ultimately fire `start-context` event
291 this._firstNodeCreated = false;
292 }, {
293 request: { reload: Option(0, "boolean") },
294 oneway: true
295 }),
297 /**
298 * Invoked whenever an instrumented function is called, like an AudioContext
299 * method or an AudioNode method.
300 */
301 _onContentFunctionCall: function(functionCall) {
302 let { name } = functionCall.details;
304 // All Web Audio nodes inherit from AudioNode's prototype, so
305 // hook into the `connect` and `disconnect` methods
306 if (WebAudioFront.NODE_ROUTING_METHODS.has(name)) {
307 this._handleRoutingCall(functionCall);
308 }
309 else if (WebAudioFront.NODE_CREATION_METHODS.has(name)) {
310 this._handleCreationCall(functionCall);
311 }
312 },
314 _handleRoutingCall: function(functionCall) {
315 let { caller, args, window, name } = functionCall.details;
316 let source = unwrap(caller);
317 let dest = unwrap(args[0]);
318 let isAudioParam = dest instanceof unwrap(window.AudioParam);
320 // audionode.connect(param)
321 if (name === "connect" && isAudioParam) {
322 this._onConnectParam(source, dest);
323 }
324 // audionode.connect(node)
325 else if (name === "connect") {
326 this._onConnectNode(source, dest);
327 }
328 // audionode.disconnect()
329 else if (name === "disconnect") {
330 this._onDisconnectNode(source);
331 }
332 },
334 _handleCreationCall: function (functionCall) {
335 let { caller, result } = functionCall.details;
336 // Keep track of the first node created, so we can alert
337 // the front end that an audio context is being used since
338 // we're not hooking into the constructor itself, just its
339 // instance's methods.
340 if (!this._firstNodeCreated) {
341 // Fire the start-up event if this is the first node created
342 // and trigger a `create-node` event for the context destination
343 this._onStartContext();
344 this._onCreateNode(unwrap(caller.destination));
345 this._firstNodeCreated = true;
346 }
347 this._onCreateNode(result);
348 },
350 /**
351 * Stops listening for document global changes and puts this actor
352 * to hibernation. This method is called automatically just before the
353 * actor is destroyed.
354 */
355 finalize: method(function() {
356 if (!this._initialized) {
357 return;
358 }
359 this._initialized = false;
360 this._callWatcher.eraseRecording();
362 this._callWatcher.finalize();
363 this._callWatcher = null;
364 }, {
365 oneway: true
366 }),
368 /**
369 * Events emitted by this actor.
370 */
371 events: {
372 "start-context": {
373 type: "startContext"
374 },
375 "connect-node": {
376 type: "connectNode",
377 source: Option(0, "audionode"),
378 dest: Option(0, "audionode")
379 },
380 "disconnect-node": {
381 type: "disconnectNode",
382 source: Arg(0, "audionode")
383 },
384 "connect-param": {
385 type: "connectParam",
386 source: Arg(0, "audionode"),
387 param: Arg(1, "string")
388 },
389 "change-param": {
390 type: "changeParam",
391 source: Option(0, "audionode"),
392 param: Option(0, "string"),
393 value: Option(0, "string")
394 },
395 "create-node": {
396 type: "createNode",
397 source: Arg(0, "audionode")
398 }
399 },
401 /**
402 * Helper for constructing an AudioNodeActor, assigning to
403 * internal weak map, and tracking via `manage` so it is assigned
404 * an `actorID`.
405 */
406 _constructAudioNode: function (node) {
407 let actor = new AudioNodeActor(this.conn, node);
408 this.manage(actor);
409 this._nodeActors.set(node, actor);
410 return actor;
411 },
413 /**
414 * Takes an AudioNode and returns the stored actor for it.
415 * In some cases, we won't have an actor stored (for example,
416 * connecting to an AudioDestinationNode, since it's implicitly
417 * created), so make a new actor and store that.
418 */
419 _actorFor: function (node) {
420 let actor = this._nodeActors.get(node);
421 if (!actor) {
422 actor = this._constructAudioNode(node);
423 }
424 return actor;
425 },
427 /**
428 * Called on first audio node creation, signifying audio context usage
429 */
430 _onStartContext: function () {
431 events.emit(this, "start-context");
432 },
434 /**
435 * Called when one audio node is connected to another.
436 */
437 _onConnectNode: function (source, dest) {
438 let sourceActor = this._actorFor(source);
439 let destActor = this._actorFor(dest);
440 events.emit(this, "connect-node", {
441 source: sourceActor,
442 dest: destActor
443 });
444 },
446 /**
447 * Called when an audio node is connected to an audio param.
448 * Implement in bug 986705
449 */
450 _onConnectParam: function (source, dest) {
451 // TODO bug 986705
452 },
454 /**
455 * Called when an audio node is disconnected.
456 */
457 _onDisconnectNode: function (node) {
458 let actor = this._actorFor(node);
459 events.emit(this, "disconnect-node", actor);
460 },
462 /**
463 * Called when a parameter changes on an audio node
464 */
465 _onParamChange: function (node, param, value) {
466 let actor = this._actorFor(node);
467 events.emit(this, "param-change", {
468 source: actor,
469 param: param,
470 value: value
471 });
472 },
474 /**
475 * Called on node creation.
476 */
477 _onCreateNode: function (node) {
478 let actor = this._constructAudioNode(node);
479 events.emit(this, "create-node", actor);
480 }
481 });
483 /**
484 * The corresponding Front object for the WebAudioActor.
485 */
486 let WebAudioFront = exports.WebAudioFront = protocol.FrontClass(WebAudioActor, {
487 initialize: function(client, { webaudioActor }) {
488 protocol.Front.prototype.initialize.call(this, client, { actor: webaudioActor });
489 client.addActorPool(this);
490 this.manage(this);
491 }
492 });
494 WebAudioFront.NODE_CREATION_METHODS = new Set(NODE_CREATION_METHODS);
495 WebAudioFront.NODE_ROUTING_METHODS = new Set(NODE_ROUTING_METHODS);
497 /**
498 * Determines whether or not property is an AudioParam.
499 *
500 * @param AudioNode node
501 * An AudioNode.
502 * @param String prop
503 * Property of `node` to evaluate to see if it's an AudioParam.
504 * @return Boolean
505 */
506 function isAudioParam (node, prop) {
507 return /AudioParam/.test(node[prop].toString());
508 }
510 /**
511 * Takes an `Error` object and constructs a JSON-able response
512 *
513 * @param Error err
514 * A TypeError, RangeError, etc.
515 * @return Object
516 */
517 function constructError (err) {
518 return {
519 message: err.message,
520 type: err.constructor.name
521 };
522 }
524 function unwrap (obj) {
525 return XPCNativeWrapper.unwrap(obj);
526 }