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
13 * @param {boolean} loggingEnabled Whether audio logging is enabled.
15 * @extends {cr.EventTarget}
17 function NaClManager(loggingEnabled) {
19 * Current state of this manager.
20 * @private {hotword.NaClManager.ManagerState_}
22 this.recognizerState_ = ManagerState_.UNINITIALIZED;
25 * The window.timeout ID associated with a pending message.
28 this.naclTimeoutId_ = null;
31 * The expected message that will cancel the current timeout.
34 this.expectingMessage_ = null;
37 * Whether the plugin will be started as soon as it stops.
40 this.restartOnStop_ = false;
43 * NaCl plugin element on extension background page.
44 * @private {?HTMLEmbedElement}
49 * URL containing hotword-model data file.
55 * Media stream containing an audio input track.
56 * @private {?MediaStream}
61 * The mode to start the recognizer in.
62 * @private {?chrome.hotwordPrivate.RecognizerStartMode}
64 this.startMode_ = hotword.constants.RecognizerStartMode.NORMAL;
67 * Whether audio logging is enabled.
70 this.loggingEnabled_ = loggingEnabled;
73 * Audio log of X seconds before hotword triggered.
76 this.preambleLog_ = null;
80 * States this manager can be in. Since messages to/from the plugin are
81 * asynchronous (and potentially queued), it's not possible to know what state
82 * the plugin is in. However, track a state machine for NaClManager based on
83 * what messages are sent/received.
87 NaClManager.ManagerState_ = {
97 var ManagerState_ = NaClManager.ManagerState_;
98 var Error_ = hotword.constants.Error;
99 var UmaNaClMessageTimeout_ = hotword.constants.UmaNaClMessageTimeout;
100 var UmaNaClPluginLoadResult_ = hotword.constants.UmaNaClPluginLoadResult;
102 NaClManager.prototype.__proto__ = cr.EventTarget.prototype;
105 * Called when an error occurs. Dispatches an event.
106 * @param {!hotword.constants.Error} error
109 NaClManager.prototype.handleError_ = function(error) {
110 var event = new Event(hotword.constants.Event.ERROR);
112 this.dispatchEvent(event);
116 * Record the result of loading the NaCl plugin to UMA.
117 * @param {!hotword.constants.UmaNaClPluginLoadResult} error
120 NaClManager.prototype.logPluginLoadResult_ = function(error) {
121 hotword.metrics.recordEnum(
122 hotword.constants.UmaMetrics.NACL_PLUGIN_LOAD_RESULT,
124 UmaNaClPluginLoadResult_.MAX);
128 * Set a timeout. Only allow one timeout to exist at any given time.
129 * @param {!function()} func
130 * @param {number} timeout
133 NaClManager.prototype.setTimeout_ = function(func, timeout) {
134 assert(!this.naclTimeoutId_, 'Timeout already exists');
135 this.naclTimeoutId_ = window.setTimeout(
137 this.naclTimeoutId_ = null;
139 }.bind(this), timeout);
143 * Clears the current timeout.
146 NaClManager.prototype.clearTimeout_ = function() {
147 window.clearTimeout(this.naclTimeoutId_);
148 this.naclTimeoutId_ = null;
152 * Starts a stopped or stopping hotword recognizer (NaCl plugin).
153 * @param {hotword.constants.RecognizerStartMode} mode The mode to start the
156 NaClManager.prototype.startRecognizer = function(mode) {
157 this.startMode_ = mode;
158 if (this.recognizerState_ == ManagerState_.STOPPED) {
159 this.preambleLog_ = null;
160 this.recognizerState_ = ManagerState_.STARTING;
161 if (mode == hotword.constants.RecognizerStartMode.NEW_MODEL) {
162 hotword.debug('Starting Recognizer in START training mode');
163 this.sendDataToPlugin_(hotword.constants.NaClPlugin.BEGIN_SPEAKER_MODEL);
164 } else if (mode == hotword.constants.RecognizerStartMode.ADAPT_MODEL) {
165 hotword.debug('Starting Recognizer in ADAPT training mode');
166 this.sendDataToPlugin_(hotword.constants.NaClPlugin.ADAPT_SPEAKER_MODEL);
168 hotword.debug('Starting Recognizer in NORMAL mode');
169 this.sendDataToPlugin_(hotword.constants.NaClPlugin.RESTART);
171 // Normally, there would be a waitForMessage_(READY_FOR_AUDIO) here.
172 // However, this message is sent the first time audio data is read and in
173 // some cases (ie. using the hotword stream), this won't happen until a
174 // potential hotword trigger is seen. Having a waitForMessage_() would time
175 // out in this case, so just leave it out. This ends up sacrificing a bit of
176 // error detection in the non-hotword-stream case, but I think we can live
178 } else if (this.recognizerState_ == ManagerState_.STOPPING) {
179 // Wait until the plugin is stopped before trying to start it.
180 this.restartOnStop_ = true;
182 throw 'Attempting to start NaCl recogniser not in STOPPED or STOPPING ' +
188 * Stops the hotword recognizer.
190 NaClManager.prototype.stopRecognizer = function() {
191 if (this.recognizerState_ == ManagerState_.STARTING) {
192 // If the recognizer is stopped before it finishes starting, it causes an
193 // assertion to be raised in waitForMessage_() since we're waiting for the
194 // READY_FOR_AUDIO message. Clear the current timeout and expecting message
195 // since we no longer expect it and may never receive it.
196 this.clearTimeout_();
197 this.expectingMessage_ = null;
199 this.sendDataToPlugin_(hotword.constants.NaClPlugin.STOP);
200 this.recognizerState_ = ManagerState_.STOPPING;
201 this.waitForMessage_(hotword.constants.TimeoutMs.NORMAL,
202 hotword.constants.NaClPlugin.STOPPED);
206 * Saves the speaker model.
208 NaClManager.prototype.finalizeSpeakerModel = function() {
209 if (this.recognizerState_ == ManagerState_.UNINITIALIZED ||
210 this.recognizerState_ == ManagerState_.ERROR ||
211 this.recognizerState_ == ManagerState_.SHUTDOWN ||
212 this.recognizerState_ == ManagerState_.LOADING) {
215 this.sendDataToPlugin_(hotword.constants.NaClPlugin.FINISH_SPEAKER_MODEL);
219 * Checks whether the file at the given path exists.
220 * @param {!string} path Path to a file. Can be any valid URL.
221 * @return {boolean} True if the patch exists.
224 NaClManager.prototype.fileExists_ = function(path) {
225 var xhr = new XMLHttpRequest();
226 xhr.open('HEAD', path, false);
232 if (xhr.readyState != xhr.DONE || xhr.status != 200) {
239 * Creates and returns a list of possible languages to check for hotword
241 * @return {!Array<string>} Array of languages.
244 NaClManager.prototype.getPossibleLanguages_ = function() {
245 // Create array used to search first for language-country, if not found then
246 // search for language, if not found then no language (empty string).
247 // For example, search for 'en-us', then 'en', then ''.
248 var langs = new Array();
249 if (hotword.constants.UI_LANGUAGE) {
250 // Chrome webstore doesn't support uppercase path: crbug.com/353407
251 var language = hotword.constants.UI_LANGUAGE.toLowerCase();
252 langs.push(language); // Example: 'en-us'.
253 // Remove country to add just the language to array.
254 var hyphen = language.lastIndexOf('-');
256 langs.push(language.substr(0, hyphen)); // Example: 'en'.
264 * Creates a NaCl plugin object and attaches it to the page.
265 * @param {!string} src Location of the plugin.
266 * @return {!HTMLEmbedElement} NaCl plugin DOM object.
269 NaClManager.prototype.createPlugin_ = function(src) {
270 var plugin = /** @type {HTMLEmbedElement} */(document.createElement('embed'));
272 plugin.type = 'application/x-nacl';
273 document.body.appendChild(plugin);
278 * Initializes the NaCl manager.
279 * @param {!string} naclArch Either 'arm', 'x86-32' or 'x86-64'.
280 * @param {!MediaStream} stream A stream containing an audio source track.
281 * @return {boolean} True if the successful.
283 NaClManager.prototype.initialize = function(naclArch, stream) {
284 assert(this.recognizerState_ == ManagerState_.UNINITIALIZED,
285 'Recognizer not in uninitialized state. State: ' +
286 this.recognizerState_);
287 var langs = this.getPossibleLanguages_();
289 // For country-lang variations. For example, when combined with path it will
290 // attempt to find: '/x86-32_en-gb/', else '/x86-32_en/', else '/x86-32_/'.
291 for (i = 0; i < langs.length; i++) {
292 var folder = hotword.constants.SHARED_MODULE_ROOT + '/_platform_specific/' +
293 naclArch + '_' + langs[i] + '/';
294 var dataSrc = folder + hotword.constants.File.RECOGNIZER_CONFIG;
295 var pluginSrc = hotword.constants.SHARED_MODULE_ROOT + '/hotword_' +
297 var dataExists = this.fileExists_(dataSrc) && this.fileExists_(pluginSrc);
302 var plugin = this.createPlugin_(pluginSrc);
303 this.plugin_ = plugin;
304 if (!this.plugin_ || !this.plugin_.postMessage) {
305 document.body.removeChild(this.plugin_);
306 this.recognizerState_ = ManagerState_.ERROR;
309 this.modelUrl_ = chrome.extension.getURL(dataSrc);
310 this.stream_ = stream;
311 this.recognizerState_ = ManagerState_.LOADING;
313 plugin.addEventListener('message',
314 this.handlePluginMessage_.bind(this),
317 plugin.addEventListener('crash',
319 this.handleError_(Error_.NACL_CRASH);
320 this.logPluginLoadResult_(
321 UmaNaClPluginLoadResult_.CRASH);
326 this.recognizerState_ = ManagerState_.ERROR;
327 this.logPluginLoadResult_(UmaNaClPluginLoadResult_.NO_MODULE_FOUND);
332 * Shuts down the NaCl plugin and frees all resources.
334 NaClManager.prototype.shutdown = function() {
335 if (this.plugin_ != null) {
336 document.body.removeChild(this.plugin_);
339 this.clearTimeout_();
340 this.recognizerState_ = ManagerState_.SHUTDOWN;
347 * Sends data to the NaCl plugin.
348 * @param {!string|!MediaStreamTrack} data Command to be sent to NaCl plugin.
351 NaClManager.prototype.sendDataToPlugin_ = function(data) {
352 assert(this.recognizerState_ != ManagerState_.UNINITIALIZED,
353 'Recognizer in uninitialized state');
354 this.plugin_.postMessage(data);
358 * Waits, with a timeout, for a message to be received from the plugin. If the
359 * message is not seen within the timeout, dispatch an 'error' event and go into
361 * @param {number} timeout Timeout, in milliseconds, to wait for the message.
362 * @param {!string} message Message to wait for.
365 NaClManager.prototype.waitForMessage_ = function(timeout, message) {
366 assert(this.expectingMessage_ == null, 'Cannot wait for message: ' +
367 message + ', already waiting for message ' + this.expectingMessage_);
370 this.recognizerState_ = ManagerState_.ERROR;
371 this.handleError_(Error_.TIMEOUT);
372 switch (this.expectingMessage_) {
373 case hotword.constants.NaClPlugin.REQUEST_MODEL:
374 var metricValue = UmaNaClMessageTimeout_.REQUEST_MODEL;
376 case hotword.constants.NaClPlugin.MODEL_LOADED:
377 var metricValue = UmaNaClMessageTimeout_.MODEL_LOADED;
379 case hotword.constants.NaClPlugin.READY_FOR_AUDIO:
380 var metricValue = UmaNaClMessageTimeout_.READY_FOR_AUDIO;
382 case hotword.constants.NaClPlugin.STOPPED:
383 var metricValue = UmaNaClMessageTimeout_.STOPPED;
385 case hotword.constants.NaClPlugin.HOTWORD_DETECTED:
386 var metricValue = UmaNaClMessageTimeout_.HOTWORD_DETECTED;
388 case hotword.constants.NaClPlugin.MS_CONFIGURED:
389 var metricValue = UmaNaClMessageTimeout_.MS_CONFIGURED;
392 hotword.metrics.recordEnum(
393 hotword.constants.UmaMetrics.NACL_MESSAGE_TIMEOUT,
395 UmaNaClMessageTimeout_.MAX);
396 }.bind(this), timeout);
397 this.expectingMessage_ = message;
401 * Called when a message is received from the plugin. If we're waiting for that
402 * message, cancel the pending timeout.
403 * @param {string} message Message received.
406 NaClManager.prototype.receivedMessage_ = function(message) {
407 if (message == this.expectingMessage_) {
408 this.clearTimeout_();
409 this.expectingMessage_ = null;
414 * Handle a REQUEST_MODEL message from the plugin.
415 * The plugin sends this message immediately after starting.
418 NaClManager.prototype.handleRequestModel_ = function() {
419 if (this.recognizerState_ != ManagerState_.LOADING) {
422 this.logPluginLoadResult_(UmaNaClPluginLoadResult_.SUCCESS);
423 this.sendDataToPlugin_(
424 hotword.constants.NaClPlugin.MODEL_PREFIX + this.modelUrl_);
425 this.waitForMessage_(hotword.constants.TimeoutMs.LONG,
426 hotword.constants.NaClPlugin.MODEL_LOADED);
428 // Configure logging in the plugin. This can be configured any time before
429 // starting the recognizer, and now is as good a time as any.
430 if (this.loggingEnabled_) {
431 this.sendDataToPlugin_(
432 hotword.constants.NaClPlugin.LOG + ':' +
433 hotword.constants.AUDIO_LOG_SECONDS);
438 * Handle a MODEL_LOADED message from the plugin.
439 * The plugin sends this message after successfully loading the language model.
442 NaClManager.prototype.handleModelLoaded_ = function() {
443 if (this.recognizerState_ != ManagerState_.LOADING) {
446 this.sendDataToPlugin_(this.stream_.getAudioTracks()[0]);
447 this.waitForMessage_(hotword.constants.TimeoutMs.LONG,
448 hotword.constants.NaClPlugin.MS_CONFIGURED);
452 * Handle a MS_CONFIGURED message from the plugin.
453 * The plugin sends this message after successfully configuring the audio input
457 NaClManager.prototype.handleMsConfigured_ = function() {
458 if (this.recognizerState_ != ManagerState_.LOADING) {
461 this.recognizerState_ = ManagerState_.STOPPED;
462 this.dispatchEvent(new Event(hotword.constants.Event.READY));
466 * Handle a READY_FOR_AUDIO message from the plugin.
467 * The plugin sends this message after the recognizer is started and
468 * successfully receives and processes audio data.
471 NaClManager.prototype.handleReadyForAudio_ = function() {
472 if (this.recognizerState_ != ManagerState_.STARTING) {
475 this.recognizerState_ = ManagerState_.RUNNING;
479 * Handle a HOTWORD_DETECTED message from the plugin.
480 * The plugin sends this message after detecting the hotword.
483 NaClManager.prototype.handleHotwordDetected_ = function() {
484 if (this.recognizerState_ != ManagerState_.RUNNING) {
487 // We'll receive a STOPPED message very soon.
488 this.recognizerState_ = ManagerState_.STOPPING;
489 this.waitForMessage_(hotword.constants.TimeoutMs.NORMAL,
490 hotword.constants.NaClPlugin.STOPPED);
491 var event = new Event(hotword.constants.Event.TRIGGER);
492 event.log = this.preambleLog_;
493 this.dispatchEvent(event);
497 * Handle a STOPPED message from the plugin.
498 * This plugin sends this message after stopping the recognizer. This can happen
499 * either in response to a stop request, or after the hotword is detected.
502 NaClManager.prototype.handleStopped_ = function() {
503 this.recognizerState_ = ManagerState_.STOPPED;
504 if (this.restartOnStop_) {
505 this.restartOnStop_ = false;
506 this.startRecognizer(this.startMode_);
511 * Handle a TIMEOUT message from the plugin.
512 * The plugin sends this message when it thinks the stream is from a DSP and
513 * a hotword wasn't detected within a timeout period after arrival of the first
517 NaClManager.prototype.handleTimeout_ = function() {
518 if (this.recognizerState_ != ManagerState_.RUNNING) {
521 this.recognizerState_ = ManagerState_.STOPPED;
522 this.dispatchEvent(new Event(hotword.constants.Event.TIMEOUT));
526 * Handle a SPEAKER_MODEL_SAVED message from the plugin.
527 * The plugin sends this message after writing the model to a file.
530 NaClManager.prototype.handleSpeakerModelSaved_ = function() {
531 this.dispatchEvent(new Event(hotword.constants.Event.SPEAKER_MODEL_SAVED));
535 * Handles a message from the NaCl plugin.
536 * @param {!Event} msg Message from NaCl plugin.
539 NaClManager.prototype.handlePluginMessage_ = function(msg) {
541 if (typeof(msg['data']) == 'object') {
542 // Save the preamble for delivery to the trigger handler when the trigger
544 this.preambleLog_ = msg['data'];
547 this.receivedMessage_(msg['data']);
548 switch (msg['data']) {
549 case hotword.constants.NaClPlugin.REQUEST_MODEL:
550 this.handleRequestModel_();
552 case hotword.constants.NaClPlugin.MODEL_LOADED:
553 this.handleModelLoaded_();
555 case hotword.constants.NaClPlugin.MS_CONFIGURED:
556 this.handleMsConfigured_();
558 case hotword.constants.NaClPlugin.READY_FOR_AUDIO:
559 this.handleReadyForAudio_();
561 case hotword.constants.NaClPlugin.HOTWORD_DETECTED:
562 this.handleHotwordDetected_();
564 case hotword.constants.NaClPlugin.STOPPED:
565 this.handleStopped_();
567 case hotword.constants.NaClPlugin.TIMEOUT:
568 this.handleTimeout_();
570 case hotword.constants.NaClPlugin.SPEAKER_MODEL_SAVED:
571 this.handleSpeakerModelSaved_();
578 NaClManager: NaClManager