Chromium Code Reviews| 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 |
| +}; |
| + |
| +}); |