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

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: Rebase. 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
« no previous file with comments | « chrome/browser/resources/hotword/manifest.json ('k') | no next file » | no next file with comments »
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
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..275ce96c46649cf0a75213b062e8109afab77400
--- /dev/null
+++ b/chrome/browser/resources/hotword/nacl_manager.js
@@ -0,0 +1,435 @@
+// 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() {
+ /**
+ * Current state of this manager.
+ * @private {hotword.NaClManager.ManagerState_}
+ */
+ this.recognizerState_ = ManagerState_.UNINITIALIZED;
+
+ /**
+ * The window.timeout ID associated with a pending message.
+ * @private {?number}
+ */
+ this.naclTimeoutId_ = null;
+
+ /**
+ * The expected message that will cancel the current timeout.
+ * @private {?string}
+ */
+ this.expectingMessage_ = null;
+
+ /**
+ * Whether the plugin will be started as soon as it stops.
+ * @private {boolean}
+ */
+ this.restartOnStop_ = false;
+
+ /**
+ * NaCl plugin element on extension background page.
+ * @private {?Nacl}
+ */
+ this.plugin_ = null;
+
+ /**
+ * URL containing hotword-model data file.
+ * @private {string}
+ */
+ this.modelUrl_ = '';
+
+ /**
+ * Media stream containing an audio input track.
+ * @private {?MediaStream}
+ */
+ this.stream_ = null;
+};
+
+/**
+ * States this manager can be in. Since messages to/from the plugin are
+ * asynchronous (and potentially queued), it's not possible to know what state
+ * the plugin is in. However, track a state machine for NaClManager based on
+ * what messages are sent/received.
+ * @enum {number}
+ * @private
+ */
+NaClManager.ManagerState_ = {
+ UNINITIALIZED: 0,
+ LOADING: 1,
+ STOPPING: 2,
+ STOPPED: 3,
+ STARTING: 4,
+ RUNNING: 5,
+ ERROR: 6,
+ SHUTDOWN: 7,
+};
+var ManagerState_ = NaClManager.ManagerState_;
+var Error_ = hotword.constants.Error;
+
+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
+ * @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) {
+ this.recognizerState_ = ManagerState_.STARTING;
+ this.sendDataToPlugin_(hotword.constants.NaClPlugin.RESTART);
+ this.waitForMessage_(hotword.constants.TimeoutMs.LONG,
+ 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.TimeoutMs.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;
+};
+
+/**
+ * Creates and returns a list of possible languages to check for hotword
+ * support.
+ * @return {!Array.<string>} Array of languages.
+ * @private
+ */
+NaClManager.prototype.getPossibleLanguages_ = function() {
+ // 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('');
+ return langs;
+};
+
+/**
+ * Creates a NaCl plugin object and attaches it to the page.
+ * @param {!string} src Location of the plugin.
+ * @return {!Nacl} NaCl plugin DOM object.
+ * @private
+ */
+NaClManager.prototype.createPlugin_ = function(src) {
+ var plugin = document.createElement('embed');
+ plugin.src = src;
+ plugin.type = 'application/x-nacl';
+ document.body.appendChild(plugin);
+ return plugin;
+};
+
+/**
+ * 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) {
+ assert(this.recognizerState_ == ManagerState_.UNINITIALIZED);
+ var langs = this.getPossibleLanguages_();
+ 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 pluginSrc = hotword.constants.SHARED_MODULE_ROOT + '/hotword_' +
+ langs[i] + '.nmf';
+ var dataExists = this.fileExists_(dataSrc) && this.fileExists_(pluginSrc);
+ if (!dataExists) {
+ continue;
+ }
+
+ var plugin = this.createPlugin_(pluginSrc);
+ this.plugin_ = /** @type {Nacl} */ (plugin);
+ if (!this.plugin_ || !this.plugin_.postMessage) {
+ document.body.removeChild(this.plugin_);
+ 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',
+ this.handleError_.bind(this, Error_.NACL_CRASH),
+ false);
+ return true;
+ }
+ this.recognizerState_ = ManagerState_.ERROR;
+ return false;
+};
+
+/**
+ * Shuts down the NaCl plugin and frees all resources.
+ */
+NaClManager.prototype.shutdown = function() {
+ if (this.plugin_ != null) {
+ document.body.removeChild(this.plugin_);
+ this.plugin_ = null;
+ }
+ this.clearTimeout_();
+ this.recognizerState_ = ManagerState_.SHUTDOWN;
+ this.stream_ = null;
+};
+
+/**
+ * Sends data to the NaCl plugin.
+ * @param {!string} data Command to be sent to NaCl plugin.
+ * @private
+ */
+NaClManager.prototype.sendDataToPlugin_ = function(data) {
+ assert(this.recognizerState_ != ManagerState_.UNINITIALIZED);
+ this.plugin_.postMessage(data);
+};
+
+/**
+ * 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) {
+ assert(this.expectingMessage_ == null,
+ 'Already waiting for message ' + this.expectingMessage_);
+ this.setTimeout_(
+ function() {
+ 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;
+ }
+};
+
+/**
+ * Handle a REQUEST_MODEL message from the plugin.
+ * The plugin sends this message immediately after starting.
+ * @private
+ */
+NaClManager.prototype.handleRequestModel_ = function() {
+ if (this.recognizerState_ != ManagerState_.LOADING) {
+ return;
+ }
+ this.sendDataToPlugin_(
+ hotword.constants.NaClPlugin.MODEL_PREFIX + this.modelUrl_);
+ this.waitForMessage_(hotword.constants.TimeoutMs.LONG,
+ hotword.constants.NaClPlugin.MODEL_LOADED);
+};
+
+/**
+ * Handle a MODEL_LOADED message from the plugin.
+ * The plugin sends this message after successfully loading the language model.
+ * @private
+ */
+NaClManager.prototype.handleModelLoaded_ = function() {
+ if (this.recognizerState_ != ManagerState_.LOADING) {
+ return;
+ }
+ this.sendDataToPlugin_(this.stream_.getAudioTracks()[0]);
+ this.waitForMessage_(hotword.constants.TimeoutMs.LONG,
+ hotword.constants.NaClPlugin.MS_CONFIGURED);
+};
+
+/**
+ * Handle a MS_CONFIGURED message from the plugin.
+ * The plugin sends this message after successfully configuring the audio input
+ * stream.
+ * @private
+ */
+NaClManager.prototype.handleMsConfigured_ = function() {
+ if (this.recognizerState_ != ManagerState_.LOADING) {
+ return;
+ }
+ this.recognizerState_ = ManagerState_.STOPPED;
+ this.dispatchEvent(new Event(hotword.constants.Event.READY));
+};
+
+/**
+ * Handle a READY_FOR_AUDIO message from the plugin.
+ * The plugin sends this message after the recognizer is started and
+ * successfully receives and processes audio data.
+ * @private
+ */
+NaClManager.prototype.handleReadyForAudio_ = function() {
+ if (this.recognizerState_ != ManagerState_.STARTING) {
+ return;
+ }
+ this.recognizerState_ = ManagerState_.RUNNING;
+};
+
+/**
+ * Handle a HOTWORD_DETECTED message from the plugin.
+ * The plugin sends this message after detecting the hotword.
+ * @private
+ */
+NaClManager.prototype.handleHotwordDetected_ = function() {
+ if (this.recognizerState_ != ManagerState_.RUNNING) {
+ return;
+ }
+ // We'll receive a STOPPED message very soon.
+ this.recognizerState_ = ManagerState_.STOPPING;
+ this.waitForMessage_(hotword.constants.TimeoutMs.NORMAL,
+ hotword.constants.NaClPlugin.STOPPED);
+ this.dispatchEvent(new Event(hotword.constants.Event.TRIGGER));
+};
+
+/**
+ * Handle a STOPPED message from the plugin.
+ * This plugin sends this message after stopping the recognizer. This can happen
+ * either in response to a stop request, or after the hotword is detected.
+ * @private
+ */
+NaClManager.prototype.handleStopped_ = function() {
+ this.recognizerState_ = ManagerState_.STOPPED;
+ if (this.restartOnStop_) {
+ this.restartOnStop_ = false;
+ this.startRecognizer();
+ }
+};
+
+/**
+ * Handles a message from the NaCl plugin.
+ * @param {!Event} msg Message from NaCl plugin.
+ * @private
+ */
+NaClManager.prototype.handlePluginMessage_ = function(msg) {
+ if (msg['data']) {
+ this.receivedMessage_(msg['data']);
+ switch (msg['data']) {
+ case hotword.constants.NaClPlugin.REQUEST_MODEL:
+ this.handleRequestModel_();
+ break;
+ case hotword.constants.NaClPlugin.MODEL_LOADED:
+ this.handleModelLoaded_();
+ break;
+ case hotword.constants.NaClPlugin.MS_CONFIGURED:
+ this.handleMsConfigured_();
+ break;
+ case hotword.constants.NaClPlugin.READY_FOR_AUDIO:
+ this.handleReadyForAudio_();
+ break;
+ case hotword.constants.NaClPlugin.HOTWORD_DETECTED:
+ this.handleHotwordDetected_();
+ break;
+ case hotword.constants.NaClPlugin.STOPPED:
+ this.handleStopped_();
+ break;
+ }
+ }
+};
+
+return {
+ NaClManager: NaClManager
+};
+
+});
« no previous file with comments | « chrome/browser/resources/hotword/manifest.json ('k') | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698