Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(153)

Unified Diff: chrome/browser/resources/hotword/nacl_manager.js

Issue 464213002: Add a 'NaCl manager' to manage the state of the NaCl hotword detector plugin. (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: Created 6 years, 4 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
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
+};
+
+});

Powered by Google App Engine
This is Rietveld 408576698