toolkit/devtools/server/actors/webaudio.js

Sat, 03 Jan 2015 20:18:00 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Sat, 03 Jan 2015 20:18:00 +0100
branch
TOR_BUG_3246
changeset 7
129ffea94266
permissions
-rw-r--r--

Conditionally enable double key logic according to:
private browsing mode or privacy.thirdparty.isolate preference and
implement in GetCookieStringCommon and FindCookie where it counts...
With some reservations of how to convince FindCookie users to test
condition and pass a nullptr when disabling double key logic.

     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 }

mercurial