1 // Copyright 2014 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 cr.define('hotword', function() {
9 * Class used to manage the state of the NaCl recognizer plugin. Handles all
10 * control of the NaCl plugin, including creation, start, stop, trigger, and
14 * @extends {cr.EventTarget}
16 function NaClManager() {
18 * Current state of this manager.
19 * @private {hotword.NaClManager.ManagerState_}
21 this.recognizerState_ = ManagerState_.UNINITIALIZED;
24 * The window.timeout ID associated with a pending message.
27 this.naclTimeoutId_ = null;
30 * The expected message that will cancel the current timeout.
33 this.expectingMessage_ = null;
36 * Whether the plugin will be started as soon as it stops.
39 this.restartOnStop_ = false;
42 * NaCl plugin element on extension background page.
43 * @private {?HTMLEmbedElement}
48 * URL containing hotword-model data file.
54 * Media stream containing an audio input track.
55 * @private {?MediaStream}
61 * States this manager can be in. Since messages to/from the plugin are
62 * asynchronous (and potentially queued), it's not possible to know what state
63 * the plugin is in. However, track a state machine for NaClManager based on
64 * what messages are sent/received.
68 NaClManager.ManagerState_ = {
78 var ManagerState_ = NaClManager.ManagerState_;
79 var Error_ = hotword.constants.Error;
80 var UmaNaClMessageTimeout_ = hotword.constants.UmaNaClMessageTimeout;
81 var UmaNaClPluginLoadResult_ = hotword.constants.UmaNaClPluginLoadResult;
83 NaClManager.prototype.__proto__ = cr.EventTarget.prototype;
86 * Called when an error occurs. Dispatches an event.
87 * @param {!hotword.constants.Error} error
90 NaClManager.prototype.handleError_ = function(error) {
91 var event = new Event(hotword.constants.Event.ERROR);
93 this.dispatchEvent(event);
97 * Record the result of loading the NaCl plugin to UMA.
98 * @param {!hotword.constants.UmaNaClPluginLoadResult} error
101 NaClManager.prototype.logPluginLoadResult_ = function(error) {
102 hotword.metrics.recordEnum(
103 hotword.constants.UmaMetrics.NACL_PLUGIN_LOAD_RESULT,
105 UmaNaClPluginLoadResult_.MAX);
109 * @return {boolean} True if the recognizer is in a running state.
111 NaClManager.prototype.isRunning = function() {
112 return this.recognizerState_ == ManagerState_.RUNNING;
116 * Set a timeout. Only allow one timeout to exist at any given time.
117 * @param {!function()} func
118 * @param {number} timeout
121 NaClManager.prototype.setTimeout_ = function(func, timeout) {
122 assert(!this.naclTimeoutId_, 'Timeout already exists');
123 this.naclTimeoutId_ = window.setTimeout(
125 this.naclTimeoutId_ = null;
127 }.bind(this), timeout);
131 * Clears the current timeout.
134 NaClManager.prototype.clearTimeout_ = function() {
135 window.clearTimeout(this.naclTimeoutId_);
136 this.naclTimeoutId_ = null;
140 * Starts a stopped or stopping hotword recognizer (NaCl plugin).
142 NaClManager.prototype.startRecognizer = function() {
143 if (this.recognizerState_ == ManagerState_.STOPPED) {
144 this.recognizerState_ = ManagerState_.STARTING;
145 this.sendDataToPlugin_(hotword.constants.NaClPlugin.RESTART);
146 this.waitForMessage_(hotword.constants.TimeoutMs.LONG,
147 hotword.constants.NaClPlugin.READY_FOR_AUDIO);
148 } else if (this.recognizerState_ == ManagerState_.STOPPING) {
149 // Wait until the plugin is stopped before trying to start it.
150 this.restartOnStop_ = true;
152 throw 'Attempting to start NaCl recogniser not in STOPPED or STOPPING ' +
158 * Stops the hotword recognizer.
160 NaClManager.prototype.stopRecognizer = function() {
161 this.sendDataToPlugin_(hotword.constants.NaClPlugin.STOP);
162 this.recognizerState_ = ManagerState_.STOPPING;
163 this.waitForMessage_(hotword.constants.TimeoutMs.NORMAL,
164 hotword.constants.NaClPlugin.STOPPED);
168 * Checks whether the file at the given path exists.
169 * @param {!string} path Path to a file. Can be any valid URL.
170 * @return {boolean} True if the patch exists.
173 NaClManager.prototype.fileExists_ = function(path) {
174 var xhr = new XMLHttpRequest();
175 xhr.open('HEAD', path, false);
181 if (xhr.readyState != xhr.DONE || xhr.status != 200) {
188 * Creates and returns a list of possible languages to check for hotword
190 * @return {!Array.<string>} Array of languages.
193 NaClManager.prototype.getPossibleLanguages_ = function() {
194 // Create array used to search first for language-country, if not found then
195 // search for language, if not found then no language (empty string).
196 // For example, search for 'en-us', then 'en', then ''.
197 var langs = new Array();
198 if (hotword.constants.UI_LANGUAGE) {
199 // Chrome webstore doesn't support uppercase path: crbug.com/353407
200 var language = hotword.constants.UI_LANGUAGE.toLowerCase();
201 langs.push(language); // Example: 'en-us'.
202 // Remove country to add just the language to array.
203 var hyphen = language.lastIndexOf('-');
205 langs.push(language.substr(0, hyphen)); // Example: 'en'.
213 * Creates a NaCl plugin object and attaches it to the page.
214 * @param {!string} src Location of the plugin.
215 * @return {!HTMLEmbedElement} NaCl plugin DOM object.
218 NaClManager.prototype.createPlugin_ = function(src) {
219 var plugin = /** @type {HTMLEmbedElement} */(document.createElement('embed'));
221 plugin.type = 'application/x-nacl';
222 document.body.appendChild(plugin);
227 * Initializes the NaCl manager.
228 * @param {!string} naclArch Either 'arm', 'x86-32' or 'x86-64'.
229 * @param {!MediaStream} stream A stream containing an audio source track.
230 * @return {boolean} True if the successful.
232 NaClManager.prototype.initialize = function(naclArch, stream) {
233 assert(this.recognizerState_ == ManagerState_.UNINITIALIZED,
234 'Recognizer not in uninitialized state. State: ' +
235 this.recognizerState_);
236 var langs = this.getPossibleLanguages_();
238 // For country-lang variations. For example, when combined with path it will
239 // attempt to find: '/x86-32_en-gb/', else '/x86-32_en/', else '/x86-32_/'.
240 for (i = 0; i < langs.length; i++) {
241 var folder = hotword.constants.SHARED_MODULE_ROOT + '/_platform_specific/' +
242 naclArch + '_' + langs[i] + '/';
243 var dataSrc = folder + hotword.constants.File.RECOGNIZER_CONFIG;
244 var pluginSrc = hotword.constants.SHARED_MODULE_ROOT + '/hotword_' +
246 var dataExists = this.fileExists_(dataSrc) && this.fileExists_(pluginSrc);
251 var plugin = this.createPlugin_(pluginSrc);
252 this.plugin_ = plugin;
253 if (!this.plugin_ || !this.plugin_.postMessage) {
254 document.body.removeChild(this.plugin_);
255 this.recognizerState_ = ManagerState_.ERROR;
258 this.modelUrl_ = chrome.extension.getURL(dataSrc);
259 this.stream_ = stream;
260 this.recognizerState_ = ManagerState_.LOADING;
262 plugin.addEventListener('message',
263 this.handlePluginMessage_.bind(this),
266 plugin.addEventListener('crash',
268 this.handleError_(Error_.NACL_CRASH);
269 this.logPluginLoadResult_(
270 UmaNaClPluginLoadResult_.CRASH);
275 this.recognizerState_ = ManagerState_.ERROR;
276 this.logPluginLoadResult_(UmaNaClPluginLoadResult_.NO_MODULE_FOUND);
281 * Shuts down the NaCl plugin and frees all resources.
283 NaClManager.prototype.shutdown = function() {
284 if (this.plugin_ != null) {
285 document.body.removeChild(this.plugin_);
288 this.clearTimeout_();
289 this.recognizerState_ = ManagerState_.SHUTDOWN;
294 * Sends data to the NaCl plugin.
295 * @param {!string|!MediaStreamTrack} data Command to be sent to NaCl plugin.
298 NaClManager.prototype.sendDataToPlugin_ = function(data) {
299 assert(this.recognizerState_ != ManagerState_.UNINITIALIZED,
300 'Recognizer in uninitialized state');
301 this.plugin_.postMessage(data);
305 * Waits, with a timeout, for a message to be received from the plugin. If the
306 * message is not seen within the timeout, dispatch an 'error' event and go into
308 * @param {number} timeout Timeout, in milliseconds, to wait for the message.
309 * @param {!string} message Message to wait for.
312 NaClManager.prototype.waitForMessage_ = function(timeout, message) {
313 assert(this.expectingMessage_ == null,
314 'Already waiting for message ' + this.expectingMessage_);
317 this.recognizerState_ = ManagerState_.ERROR;
318 this.handleError_(Error_.TIMEOUT);
319 switch (this.expectingMessage_) {
320 case hotword.constants.NaClPlugin.REQUEST_MODEL:
321 var metricValue = UmaNaClMessageTimeout_.REQUEST_MODEL;
323 case hotword.constants.NaClPlugin.MODEL_LOADED:
324 var metricValue = UmaNaClMessageTimeout_.MODEL_LOADED;
326 case hotword.constants.NaClPlugin.READY_FOR_AUDIO:
327 var metricValue = UmaNaClMessageTimeout_.READY_FOR_AUDIO;
329 case hotword.constants.NaClPlugin.STOPPED:
330 var metricValue = UmaNaClMessageTimeout_.STOPPED;
332 case hotword.constants.NaClPlugin.HOTWORD_DETECTED:
333 var metricValue = UmaNaClMessageTimeout_.HOTWORD_DETECTED;
335 case hotword.constants.NaClPlugin.MS_CONFIGURED:
336 var metricValue = UmaNaClMessageTimeout_.MS_CONFIGURED;
339 hotword.metrics.recordEnum(
340 hotword.constants.UmaMetrics.NACL_MESSAGE_TIMEOUT,
342 UmaNaClMessageTimeout_.MAX);
343 }.bind(this), timeout);
344 this.expectingMessage_ = message;
348 * Called when a message is received from the plugin. If we're waiting for that
349 * message, cancel the pending timeout.
350 * @param {string} message Message received.
353 NaClManager.prototype.receivedMessage_ = function(message) {
354 if (message == this.expectingMessage_) {
355 this.clearTimeout_();
356 this.expectingMessage_ = null;
361 * Handle a REQUEST_MODEL message from the plugin.
362 * The plugin sends this message immediately after starting.
365 NaClManager.prototype.handleRequestModel_ = function() {
366 if (this.recognizerState_ != ManagerState_.LOADING) {
369 this.logPluginLoadResult_(UmaNaClPluginLoadResult_.SUCCESS);
370 this.sendDataToPlugin_(
371 hotword.constants.NaClPlugin.MODEL_PREFIX + this.modelUrl_);
372 this.waitForMessage_(hotword.constants.TimeoutMs.LONG,
373 hotword.constants.NaClPlugin.MODEL_LOADED);
377 * Handle a MODEL_LOADED message from the plugin.
378 * The plugin sends this message after successfully loading the language model.
381 NaClManager.prototype.handleModelLoaded_ = function() {
382 if (this.recognizerState_ != ManagerState_.LOADING) {
385 this.sendDataToPlugin_(this.stream_.getAudioTracks()[0]);
386 this.waitForMessage_(hotword.constants.TimeoutMs.LONG,
387 hotword.constants.NaClPlugin.MS_CONFIGURED);
391 * Handle a MS_CONFIGURED message from the plugin.
392 * The plugin sends this message after successfully configuring the audio input
396 NaClManager.prototype.handleMsConfigured_ = function() {
397 if (this.recognizerState_ != ManagerState_.LOADING) {
400 this.recognizerState_ = ManagerState_.STOPPED;
401 this.dispatchEvent(new Event(hotword.constants.Event.READY));
405 * Handle a READY_FOR_AUDIO message from the plugin.
406 * The plugin sends this message after the recognizer is started and
407 * successfully receives and processes audio data.
410 NaClManager.prototype.handleReadyForAudio_ = function() {
411 if (this.recognizerState_ != ManagerState_.STARTING) {
414 this.recognizerState_ = ManagerState_.RUNNING;
418 * Handle a HOTWORD_DETECTED message from the plugin.
419 * The plugin sends this message after detecting the hotword.
422 NaClManager.prototype.handleHotwordDetected_ = function() {
423 if (this.recognizerState_ != ManagerState_.RUNNING) {
426 // We'll receive a STOPPED message very soon.
427 this.recognizerState_ = ManagerState_.STOPPING;
428 this.waitForMessage_(hotword.constants.TimeoutMs.NORMAL,
429 hotword.constants.NaClPlugin.STOPPED);
430 this.dispatchEvent(new Event(hotword.constants.Event.TRIGGER));
434 * Handle a STOPPED message from the plugin.
435 * This plugin sends this message after stopping the recognizer. This can happen
436 * either in response to a stop request, or after the hotword is detected.
439 NaClManager.prototype.handleStopped_ = function() {
440 this.recognizerState_ = ManagerState_.STOPPED;
441 if (this.restartOnStop_) {
442 this.restartOnStop_ = false;
443 this.startRecognizer();
448 * Handles a message from the NaCl plugin.
449 * @param {!Event} msg Message from NaCl plugin.
452 NaClManager.prototype.handlePluginMessage_ = function(msg) {
454 this.receivedMessage_(msg['data']);
455 switch (msg['data']) {
456 case hotword.constants.NaClPlugin.REQUEST_MODEL:
457 this.handleRequestModel_();
459 case hotword.constants.NaClPlugin.MODEL_LOADED:
460 this.handleModelLoaded_();
462 case hotword.constants.NaClPlugin.MS_CONFIGURED:
463 this.handleMsConfigured_();
465 case hotword.constants.NaClPlugin.READY_FOR_AUDIO:
466 this.handleReadyForAudio_();
468 case hotword.constants.NaClPlugin.HOTWORD_DETECTED:
469 this.handleHotwordDetected_();
471 case hotword.constants.NaClPlugin.STOPPED:
472 this.handleStopped_();
479 NaClManager: NaClManager