Index: chrome/browser/resources/hotword/nacl_manager.js |
diff --git a/chrome/browser/resources/hotword/nacl_manager.js b/chrome/browser/resources/hotword/nacl_manager.js |
new file mode 100644 |
index 0000000000000000000000000000000000000000..9052f5f4a2d461445646f3f61365fc473cec1517 |
--- /dev/null |
+++ b/chrome/browser/resources/hotword/nacl_manager.js |
@@ -0,0 +1,403 @@ |
+// Copyright (c) 2014 The Chromium Authors. All rights reserved. |
+// Use of this source code is governed by a BSD-style license that can be |
+// found in the LICENSE file. |
+ |
+cr.define('hotword', function() { |
+'use strict'; |
+ |
+/** |
+ * Class used to manage the state of the NaCl recognizer plugin. Handles all |
+ * control of the NaCl plugin, including creation, start, stop, trigger, and |
+ * shutdown. |
+ * |
+ * @constructor |
+ * @extends {cr.EventTarget} |
+ */ |
+function NaClManager() { |
+ /** |
+ * Contains messages for the NaCl recognizer plugin before it is ready to |
+ * receive messages. |
+ * @private {!Array.<string>} |
+ */ |
+ this.deferredPluginMessages_ = []; |
+ |
+ /** |
+ * Current state of this manager. |
+ * @private {hotword.NaClManager.ManagerState_} |
+ */ |
+ this.recognizerState_ = ManagerState_.OFF; |
+ |
+ /** |
+ * The window.timeout Id associated with a pending message. |
James Hawkins
2014/08/13 14:13:18
s/Id/ID/
Anand Mistry (off Chromium)
2014/08/15 04:27:01
Done.
|
+ * @type {?number} |
James Hawkins
2014/08/13 14:13:17
@private {?number}
Here and elsewhere.
Anand Mistry (off Chromium)
2014/08/15 04:27:01
Done. But I wonder, what's the different between t
|
+ * @private |
+ */ |
+ this.naclTimeoutId_ = null; |
+ |
+ /** |
+ * The expected message that will cancel the current timeout. |
+ * @type {string} |
+ * @private |
+ */ |
+ this.expectingMessage_ = null; |
+ |
+ /** |
+ * Whether the plugin will be started as soon as it stops. |
+ * @type {boolean} |
+ * @private |
+ */ |
+ this.restartOnStop_ = false; |
+ |
+ /** |
+ * NaCl plugin element on extension background page. |
+ * @private {Nacl} |
+ */ |
+ this.plugin_ = null; |
+ |
+ /** |
+ * Url containing hotword-model data file. |
James Hawkins
2014/08/13 14:13:18
s/Url/URL/
Anand Mistry (off Chromium)
2014/08/15 04:27:00
Done.
|
+ * @private {string} |
+ */ |
+ this.modelUrl_ = ''; |
+ |
+ /** |
+ * Media stream containing an audio input track. |
+ * @private {MediaStream} |
+ */ |
+ this.stream_ = null; |
+}; |
+ |
+ |
+/** |
+ * States this manager can be in. Since messages between us and the plugin are |
+ * asynchronous (and potentially queued), we don't know what state the plugin is |
James Hawkins
2014/08/13 14:13:17
Optional nit: Consider removing the ambiguous pron
Anand Mistry (off Chromium)
2014/08/15 04:27:00
Done.
|
+ * in. However, we can track a state machine for ourself based on what messages |
+ * we send/receive. |
+ * @enum {number} |
+ * @private |
+ */ |
+NaClManager.ManagerState_ = { |
+ OFF: 0, |
+ LOADING: 1, |
+ STOPPING: 2, |
+ STOPPED: 3, |
+ STARTING: 4, |
+ RUNNING: 5, |
+ ERROR: 6, |
+ SHUTDOWN: 7, |
+}; |
+var ManagerState_ = NaClManager.ManagerState_; |
+var Error_ = hotword.constants.Error; |
+ |
+ |
James Hawkins
2014/08/13 14:13:17
nit: Remove double blank line.
Anand Mistry (off Chromium)
2014/08/15 04:27:01
Done. Removed everywhere, which seems to be consis
|
+NaClManager.prototype.__proto__ = cr.EventTarget.prototype; |
+ |
+ |
+/** |
+ * Called when an error occurs. Dispatches an event. |
+ * @param {hotword.constants.Error} error |
+ * @private |
+ */ |
+NaClManager.prototype.handleError_ = function(error) { |
+ event = new Event(hotword.constants.Event.ERROR); |
+ event.data = error; |
+ this.dispatchEvent(event); |
+}; |
+ |
+ |
+/** |
+ * @return {boolean} True if the recognizer is in a running state. |
+ */ |
+NaClManager.prototype.isRunning = function() { |
+ return this.recognizerState_ == ManagerState_.RUNNING; |
+}; |
+ |
+ |
+/** |
+ * Set a timeout. Only allow one timeout to exist at any given time. |
+ * @param {Function} func |
James Hawkins
2014/08/13 14:13:17
Prefer the more-specific type 'function(param, lis
Anand Mistry (off Chromium)
2014/08/15 04:27:01
Done.
|
+ * @param {number} timeout |
+ * @private |
+ */ |
+NaClManager.prototype.setTimeout_ = function(func, timeout) { |
+ assert(!this.naclTimeoutId_); |
+ this.naclTimeoutId_ = window.setTimeout( |
+ function() { |
+ this.naclTimeoutId_ = null; |
+ func(); |
+ }.bind(this), timeout); |
+}; |
+ |
+ |
+/** |
+ * Clears the current timeout. |
+ * @private |
+ */ |
+NaClManager.prototype.clearTimeout_ = function() { |
+ window.clearTimeout(this.naclTimeoutId_); |
+ this.naclTimeoutId_ = null; |
+}; |
+ |
+ |
+/** |
+ * Starts a stopped or stopping hotword recognizer (NaCl plugin). |
+ */ |
+NaClManager.prototype.startRecognizer = function() { |
+ if (this.recognizerState_ == ManagerState_.STOPPED) { |
+ assert(this.recognizerState_ == ManagerState_.STOPPED); |
rpetterson
2014/08/15 00:41:57
Why is this assert necessary? I don't see how it w
Anand Mistry (off Chromium)
2014/08/15 04:27:01
Oops. Artifact of older iteration.
|
+ this.recognizerState_ = ManagerState_.STARTING; |
+ this.sendDataToPlugin_(hotword.constants.NaClPlugin.RESTART); |
+ this.waitForMessage_(hotword.constants.Timeout.LONG, |
James Hawkins
2014/08/13 14:13:17
nit: The start of parameter rows must align on the
Anand Mistry (off Chromium)
2014/08/15 04:27:01
Done.
|
+ hotword.constants.NaClPlugin.READY_FOR_AUDIO); |
+ } else if (this.recognizerState_ == ManagerState_.STOPPING) { |
+ // Wait until the plugin is stopped before trying to start it. |
+ this.restartOnStop_ = true; |
+ } else { |
+ throw 'Attempting to start NaCl recogniser not in STOPPED or STOPPING ' + |
+ 'state'; |
+ } |
+}; |
+ |
+ |
+/** |
+ * Stops the hotword recognizer. |
+ */ |
+NaClManager.prototype.stopRecognizer = function() { |
+ this.sendDataToPlugin_(hotword.constants.NaClPlugin.STOP); |
+ this.recognizerState_ = ManagerState_.STOPPING; |
+ this.waitForMessage_(hotword.constants.Timeout.NORMAL, |
+ hotword.constants.NaClPlugin.STOPPED); |
+}; |
+ |
+ |
+/** |
+ * Checks whether the file at the given path exists. |
+ * @param {string} path Path to a file. Can be any valid URL. |
+ * @return {boolean} True if the patch exists. |
+ * @private |
+ */ |
+NaClManager.prototype.fileExists_ = function(path) { |
+ var xhr = new XMLHttpRequest(); |
+ xhr.open('HEAD', path, false); |
+ try { |
+ xhr.send(); |
+ } catch (err) { |
+ return false; |
+ } |
+ if (xhr.readyState != xhr.DONE || xhr.status != 200) { |
+ return false; |
+ } |
+ return true; |
+}; |
+ |
+ |
+/** |
+ * Initializes the NaCl manager. |
+ * @param {string} naclArch Either 'arm', 'x86-32' or 'x86-64'. |
+ * @param {MediaStream} stream A stream containing an audio source track. |
+ * @return {boolean} True if the successful. |
+ */ |
+NaClManager.prototype.initialize = function(naclArch, stream) { |
James Hawkins
2014/08/13 14:13:17
This method is quite long. I suggest breaking it
Anand Mistry (off Chromium)
2014/08/15 04:27:01
Done.
|
+ assert(this.recognizerState_ == ManagerState_.OFF); |
+ // Create array used to search first for language-country, if not found then |
+ // search for language, if not found then no language (empty string). |
+ // For example, search for 'en-us', then 'en', then ''. |
+ var langs = new Array(); |
+ if (hotword.constants.UI_LANGUAGE) { |
+ // Chrome webstore doesn't support uppercase path: crbug.com/353407 |
+ var language = hotword.constants.UI_LANGUAGE.toLowerCase(); |
+ langs.push(language); // Example: 'en-us'. |
+ // Remove country to add just the language to array. |
+ var hyphen = language.lastIndexOf('-'); |
+ if (hyphen >= 0) { |
+ langs.push(language.substr(0, hyphen)); // Example: 'en'. |
+ } |
+ } |
+ langs.push(''); |
+ var i, j; |
+ // For country-lang variations. For example, when combined with path it will |
+ // attempt to find: '/x86-32_en-gb/', else '/x86-32_en/', else '/x86-32_/'. |
+ for (i = 0; i < langs.length; i++) { |
+ var folder = hotword.constants.SHARED_MODULE_ROOT + '/_platform_specific/' + |
+ naclArch + '_' + langs[i] + '/'; |
+ var dataSrc = folder + hotword.constants.File.RECOGNIZER_CONFIG; |
+ var dataExists = this.fileExists_(dataSrc); |
+ if (!dataExists) { |
+ console.log('File does not exist: ' + dataSrc); |
+ continue; |
+ } |
+ // If the data file exists, assume a valid NaCl nmf also exists. |
rpetterson
2014/08/15 00:41:57
how expensive is it to check?
Anand Mistry (off Chromium)
2014/08/15 04:27:00
Cheap. I've added the check.
|
+ var pluginSrc = hotword.constants.SHARED_MODULE_ROOT + '/hotword_' + |
+ langs[i] + '.nmf'; |
+ |
+ // Found the correct path. Use it to derive .nmf name and model url. |
+ // Example: If path is '_platform_specific/x86-32_en-gb/', then |
+ // use lang 'en-gb' to create embed element for 'hotword_en-gb.nmf'. |
+ var plugin = document.createElement('embed'); |
+ plugin.src = pluginSrc; |
+ plugin.type = 'application/x-nacl'; |
+ document.body.appendChild(plugin); |
+ this.plugin_ = /** @type {Nacl} */ (plugin); |
+ if (!this.plugin_ || !this.plugin_.postMessage) { |
+ this.recognizerState_ = ManagerState_.ERROR; |
+ return false; |
+ } |
+ this.modelUrl_ = chrome.extension.getURL(dataSrc); |
+ this.stream_ = stream; |
+ this.recognizerState_ = ManagerState_.LOADING; |
+ |
+ plugin.addEventListener('message', this.handlePluginMessage_.bind(this), |
+ false); |
+ |
+ plugin.addEventListener('crash', function(error) { |
+ this.handleError_(Error_.NACL_CRASH); |
+ }.bind(this), false); |
+ return true; |
+ } |
+ this.recognizerState_ = ManagerState_.ERROR; |
+ return false; |
+}; |
+ |
+ |
+/** |
+ * Shut down the NaCl plugin and free all resources. |
James Hawkins
2014/08/13 14:13:17
Shuts
Anand Mistry (off Chromium)
2014/08/15 04:27:01
Done.
|
+ */ |
+NaClManager.prototype.shutdown = function() { |
+ if (this.plugin_ != null) { |
+ document.body.removeChild(this.plugin_); |
+ this.plugin_ = null; |
+ } |
+ this.clearTimeout_(); |
+ this.recognizerState_ = ManagerState_.SHUTDOWN; |
+}; |
+ |
+ |
+/** |
+ * Sends data to the NaCl plugin. |
+ * @param {string} data Command to be sent to NaCl plugin. |
+ * @private |
+ */ |
+NaClManager.prototype.sendDataToPlugin_ = function(data) { |
+ if (this.recognizerState_ != ManagerState_.OFF) { |
+ while (this.deferredPluginMessages_.length > 0) { |
+ this.plugin_.postMessage(this.deferredPluginMessages_.shift()); |
+ } |
+ this.plugin_.postMessage(data); |
+ } else { |
+ this.deferredPluginMessages_.push(data); |
rpetterson
2014/08/15 00:41:57
Is it not possible to have deferred messages if th
Anand Mistry (off Chromium)
2014/08/15 04:27:00
Hm. We shouldn't be trying to send messages at all
|
+ } |
+}; |
+ |
+ |
+/** |
+ * Waits, with a timeout, for a message to be received from the plugin. If the |
+ * message is not seen within the timeout, dispatch an 'error' event and go into |
+ * the ERROR state. |
+ * @param {number} timeout Timeout, in milliseconds, to wait for the message. |
+ * @param {string} message Message to wait for. |
+ * @private |
+ */ |
+NaClManager.prototype.waitForMessage_ = function(timeout, message) { |
+ if (this.expectingMessage_) { |
rpetterson
2014/08/15 00:41:57
Is it possible for this function to get called twi
Anand Mistry (off Chromium)
2014/08/15 04:27:01
It used to be in an earlier iteration, but I chang
|
+ console.log('Existing wait: ' + this.expectingMessage_); |
James Hawkins
2014/08/13 14:13:17
Remove console logging.
Here and elsewhere.
Anand Mistry (off Chromium)
2014/08/15 04:27:01
Done, but I'd like to add it back (in a future CL)
|
+ this.clearTimeout_(); |
+ this.expectingMessage_ = null; |
+ } |
+ this.setTimeout_( |
+ function() { |
+ console.log('Timeout waiting for message: ' + message); |
+ this.recognizerState_ = ManagerState_.ERROR; |
+ this.handleError_(Error_.TIMEOUT); |
+ }.bind(this), timeout); |
+ this.expectingMessage_ = message; |
+}; |
+ |
+ |
+/** |
+ * Called when a message is received from the plugin. If we're waiting for that |
+ * message, cancel the pending timeout. |
+ * @param {string} message Message received. |
+ * @private |
+ */ |
+NaClManager.prototype.receivedMessage_ = function(message) { |
+ if (message == this.expectingMessage_) { |
+ this.clearTimeout_(); |
+ this.expectingMessage_ = null; |
+ } |
+}; |
+ |
+ |
+/** |
+ * Handles a message from the NaCl plugin. |
+ * @param {Event} msg Message from NaCl plugin. |
+ * @private |
+ */ |
+NaClManager.prototype.handlePluginMessage_ = function(msg) { |
James Hawkins
2014/08/13 14:13:18
I suggest also breaking up this method into sub-ha
Anand Mistry (off Chromium)
2014/08/15 04:27:00
Done.
|
+ if (msg['data']) { |
+ this.receivedMessage_(msg['data']); |
+ if (msg['data'] == hotword.constants.NaClPlugin.REQUEST_MODEL) { |
+ if (this.recognizerState_ != ManagerState_.LOADING) { |
+ console.log('Unexpected state: ' + this.recognizerState_); |
+ return; |
+ } |
+ this.sendDataToPlugin_( |
+ hotword.constants.NaClPlugin.MODEL_PREFIX + this.modelUrl_); |
+ this.waitForMessage_(hotword.constants.Timeout.LONG, |
+ hotword.constants.NaClPlugin.MODEL_LOADED); |
+ return; |
+ } |
+ if (msg['data'] == hotword.constants.NaClPlugin.MODEL_LOADED) { |
+ if (this.recognizerState_ != ManagerState_.LOADING) { |
+ console.log('Unexpected state: ' + this.recognizerState_); |
+ return; |
+ } |
+ this.sendDataToPlugin_(this.stream_.getAudioTracks()[0]); |
+ this.waitForMessage_(hotword.constants.Timeout.LONG, |
+ hotword.constants.NaClPlugin.MS_CONFIGURED); |
+ return; |
+ } |
+ if (msg['data'] == hotword.constants.NaClPlugin.MS_CONFIGURED) { |
+ if (this.recognizerState_ != ManagerState_.LOADING) { |
+ console.log('Unexpected state: ' + this.recognizerState_); |
+ return; |
+ } |
+ this.recognizerState_ = ManagerState_.STOPPED; |
+ this.dispatchEvent(new Event(hotword.constants.Event.READY)); |
+ return; |
+ } |
+ if (msg['data'] == hotword.constants.NaClPlugin.READY_FOR_AUDIO) { |
+ if (this.recognizerState_ != ManagerState_.STARTING) { |
+ console.log('Unexpected state: ' + this.recognizerState_); |
+ return; |
+ } |
+ this.recognizerState_ = ManagerState_.RUNNING; |
+ return; |
+ } |
+ if (msg['data'] == hotword.constants.NaClPlugin.HOTWORD_DETECTED) { |
+ if (this.recognizerState_ != ManagerState_.RUNNING) { |
+ console.log('Unexpected state: ' + this.recognizerState_); |
+ return; |
+ } |
+ // We'll receive a STOPPED message very soon. |
+ this.recognizerState_ = ManagerState_.STOPPING; |
+ this.waitForMessage_(hotword.constants.Timeout.NORMAL, |
+ hotword.constants.NaClPlugin.STOPPED); |
+ this.dispatchEvent(new Event(hotword.constants.Event.TRIGGER)); |
+ return; |
+ } |
+ if (msg['data'] == hotword.constants.NaClPlugin.STOPPED) { |
+ this.recognizerState_ = ManagerState_.STOPPED; |
+ if (this.restartOnStop_) { |
+ this.restartOnStop_ = false; |
+ this.startRecognizer(); |
+ } |
+ return; |
+ } |
+ } |
+}; |
+ |
+return { |
+ NaClManager: NaClManager |
+}; |
+ |
+}); |