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.
14 * @param {boolean} hotwordStream Whether the audio input stream is from a
17 * @extends {cr.EventTarget}
19 function NaClManager(loggingEnabled
, hotwordStream
) {
21 * Current state of this manager.
22 * @private {hotword.NaClManager.ManagerState_}
24 this.recognizerState_
= ManagerState_
.UNINITIALIZED
;
27 * The window.timeout ID associated with a pending message.
30 this.naclTimeoutId_
= null;
33 * The expected message that will cancel the current timeout.
36 this.expectingMessage_
= null;
39 * Whether the plugin will be started as soon as it stops.
42 this.restartOnStop_
= false;
45 * NaCl plugin element on extension background page.
46 * @private {?HTMLEmbedElement}
51 * URL containing hotword-model data file.
57 * Media stream containing an audio input track.
58 * @private {?MediaStream}
63 * The mode to start the recognizer in.
64 * @private {?chrome.hotwordPrivate.RecognizerStartMode}
66 this.startMode_
= hotword
.constants
.RecognizerStartMode
.NORMAL
;
69 * Whether audio logging is enabled.
72 this.loggingEnabled_
= loggingEnabled
;
75 * Whether the audio input stream is from a hotword stream.
78 this.hotwordStream_
= hotwordStream
;
81 * Audio log of X seconds before hotword triggered.
84 this.preambleLog_
= null;
88 * States this manager can be in. Since messages to/from the plugin are
89 * asynchronous (and potentially queued), it's not possible to know what state
90 * the plugin is in. However, track a state machine for NaClManager based on
91 * what messages are sent/received.
95 NaClManager
.ManagerState_
= {
105 var ManagerState_
= NaClManager
.ManagerState_
;
106 var Error_
= hotword
.constants
.Error
;
107 var UmaNaClMessageTimeout_
= hotword
.constants
.UmaNaClMessageTimeout
;
108 var UmaNaClPluginLoadResult_
= hotword
.constants
.UmaNaClPluginLoadResult
;
110 NaClManager
.prototype.__proto__
= cr
.EventTarget
.prototype;
113 * Called when an error occurs. Dispatches an event.
114 * @param {!hotword.constants.Error} error
117 NaClManager
.prototype.handleError_ = function(error
) {
118 var event
= new Event(hotword
.constants
.Event
.ERROR
);
120 this.dispatchEvent(event
);
124 * Record the result of loading the NaCl plugin to UMA.
125 * @param {!hotword.constants.UmaNaClPluginLoadResult} error
128 NaClManager
.prototype.logPluginLoadResult_ = function(error
) {
129 hotword
.metrics
.recordEnum(
130 hotword
.constants
.UmaMetrics
.NACL_PLUGIN_LOAD_RESULT
,
132 UmaNaClPluginLoadResult_
.MAX
);
136 * Set a timeout. Only allow one timeout to exist at any given time.
137 * @param {!function()} func
138 * @param {number} timeout
141 NaClManager
.prototype.setTimeout_ = function(func
, timeout
) {
142 assert(!this.naclTimeoutId_
, 'Timeout already exists');
143 this.naclTimeoutId_
= window
.setTimeout(
145 this.naclTimeoutId_
= null;
147 }.bind(this), timeout
);
151 * Clears the current timeout.
154 NaClManager
.prototype.clearTimeout_ = function() {
155 window
.clearTimeout(this.naclTimeoutId_
);
156 this.naclTimeoutId_
= null;
160 * Starts a stopped or stopping hotword recognizer (NaCl plugin).
161 * @param {hotword.constants.RecognizerStartMode} mode The mode to start the
164 NaClManager
.prototype.startRecognizer = function(mode
) {
165 this.startMode_
= mode
;
166 if (this.recognizerState_
== ManagerState_
.STOPPED
) {
167 this.preambleLog_
= null;
168 this.recognizerState_
= ManagerState_
.STARTING
;
169 if (mode
== hotword
.constants
.RecognizerStartMode
.NEW_MODEL
) {
170 hotword
.debug('Starting Recognizer in START training mode');
171 this.sendDataToPlugin_(hotword
.constants
.NaClPlugin
.BEGIN_SPEAKER_MODEL
);
172 } else if (mode
== hotword
.constants
.RecognizerStartMode
.ADAPT_MODEL
) {
173 hotword
.debug('Starting Recognizer in ADAPT training mode');
174 this.sendDataToPlugin_(hotword
.constants
.NaClPlugin
.ADAPT_SPEAKER_MODEL
);
176 hotword
.debug('Starting Recognizer in NORMAL mode');
177 this.sendDataToPlugin_(hotword
.constants
.NaClPlugin
.RESTART
);
179 // Normally, there would be a waitForMessage_(READY_FOR_AUDIO) here.
180 // However, this message is sent the first time audio data is read and in
181 // some cases (ie. using the hotword stream), this won't happen until a
182 // potential hotword trigger is seen. Having a waitForMessage_() would time
183 // out in this case, so just leave it out. This ends up sacrificing a bit of
184 // error detection in the non-hotword-stream case, but I think we can live
186 } else if (this.recognizerState_
== ManagerState_
.STOPPING
) {
187 // Wait until the plugin is stopped before trying to start it.
188 this.restartOnStop_
= true;
190 throw 'Attempting to start NaCl recogniser not in STOPPED or STOPPING ' +
196 * Stops the hotword recognizer.
198 NaClManager
.prototype.stopRecognizer = function() {
199 if (this.recognizerState_
== ManagerState_
.STARTING
) {
200 // If the recognizer is stopped before it finishes starting, it causes an
201 // assertion to be raised in waitForMessage_() since we're waiting for the
202 // READY_FOR_AUDIO message. Clear the current timeout and expecting message
203 // since we no longer expect it and may never receive it.
204 this.clearTimeout_();
205 this.expectingMessage_
= null;
207 this.sendDataToPlugin_(hotword
.constants
.NaClPlugin
.STOP
);
208 this.recognizerState_
= ManagerState_
.STOPPING
;
209 this.waitForMessage_(hotword
.constants
.TimeoutMs
.NORMAL
,
210 hotword
.constants
.NaClPlugin
.STOPPED
);
214 * Saves the speaker model.
216 NaClManager
.prototype.finalizeSpeakerModel = function() {
217 if (this.recognizerState_
== ManagerState_
.UNINITIALIZED
||
218 this.recognizerState_
== ManagerState_
.ERROR
||
219 this.recognizerState_
== ManagerState_
.SHUTDOWN
||
220 this.recognizerState_
== ManagerState_
.LOADING
) {
223 this.sendDataToPlugin_(hotword
.constants
.NaClPlugin
.FINISH_SPEAKER_MODEL
);
227 * Checks whether the file at the given path exists.
228 * @param {!string} path Path to a file. Can be any valid URL.
229 * @return {boolean} True if the patch exists.
232 NaClManager
.prototype.fileExists_ = function(path
) {
233 var xhr
= new XMLHttpRequest();
234 xhr
.open('HEAD', path
, false);
240 if (xhr
.readyState
!= xhr
.DONE
|| xhr
.status
!= 200) {
247 * Creates and returns a list of possible languages to check for hotword
249 * @return {!Array<string>} Array of languages.
252 NaClManager
.prototype.getPossibleLanguages_ = function() {
253 // Create array used to search first for language-country, if not found then
254 // search for language, if not found then no language (empty string).
255 // For example, search for 'en-us', then 'en', then ''.
256 var langs
= new Array();
257 if (hotword
.constants
.UI_LANGUAGE
) {
258 // Chrome webstore doesn't support uppercase path: crbug.com/353407
259 var language
= hotword
.constants
.UI_LANGUAGE
.toLowerCase();
260 langs
.push(language
); // Example: 'en-us'.
261 // Remove country to add just the language to array.
262 var hyphen
= language
.lastIndexOf('-');
264 langs
.push(language
.substr(0, hyphen
)); // Example: 'en'.
272 * Creates a NaCl plugin object and attaches it to the page.
273 * @param {!string} src Location of the plugin.
274 * @return {!HTMLEmbedElement} NaCl plugin DOM object.
277 NaClManager
.prototype.createPlugin_ = function(src
) {
278 var plugin
= /** @type {HTMLEmbedElement} */(document
.createElement('embed'));
280 plugin
.type
= 'application/x-nacl';
281 document
.body
.appendChild(plugin
);
286 * Initializes the NaCl manager.
287 * @param {!string} naclArch Either 'arm', 'x86-32' or 'x86-64'.
288 * @param {!MediaStream} stream A stream containing an audio source track.
289 * @return {boolean} True if the successful.
291 NaClManager
.prototype.initialize = function(naclArch
, stream
) {
292 assert(this.recognizerState_
== ManagerState_
.UNINITIALIZED
,
293 'Recognizer not in uninitialized state. State: ' +
294 this.recognizerState_
);
295 assert(this.plugin_
== null);
296 var langs
= this.getPossibleLanguages_();
298 // For country-lang variations. For example, when combined with path it will
299 // attempt to find: '/x86-32_en-gb/', else '/x86-32_en/', else '/x86-32_/'.
300 for (i
= 0; i
< langs
.length
; i
++) {
301 var folder
= hotword
.constants
.SHARED_MODULE_ROOT
+ '/_platform_specific/' +
302 naclArch
+ '_' + langs
[i
] + '/';
303 var dataSrc
= folder
+ hotword
.constants
.File
.RECOGNIZER_CONFIG
;
304 var pluginSrc
= hotword
.constants
.SHARED_MODULE_ROOT
+ '/hotword_' +
306 var dataExists
= this.fileExists_(dataSrc
) && this.fileExists_(pluginSrc
);
311 var plugin
= this.createPlugin_(pluginSrc
);
312 if (!plugin
|| !plugin
.postMessage
) {
313 document
.body
.removeChild(plugin
);
314 this.recognizerState_
= ManagerState_
.ERROR
;
317 this.plugin_
= plugin
;
318 this.modelUrl_
= chrome
.extension
.getURL(dataSrc
);
319 this.stream_
= stream
;
320 this.recognizerState_
= ManagerState_
.LOADING
;
322 plugin
.addEventListener('message',
323 this.handlePluginMessage_
.bind(this),
326 plugin
.addEventListener('crash',
328 this.handleError_(Error_
.NACL_CRASH
);
329 this.logPluginLoadResult_(
330 UmaNaClPluginLoadResult_
.CRASH
);
335 this.recognizerState_
= ManagerState_
.ERROR
;
336 this.logPluginLoadResult_(UmaNaClPluginLoadResult_
.NO_MODULE_FOUND
);
341 * Shuts down the NaCl plugin and frees all resources.
343 NaClManager
.prototype.shutdown = function() {
344 if (this.plugin_
!= null) {
345 document
.body
.removeChild(this.plugin_
);
348 this.clearTimeout_();
349 this.recognizerState_
= ManagerState_
.SHUTDOWN
;
356 * Sends data to the NaCl plugin.
357 * @param {!string|!MediaStreamTrack} data Command to be sent to NaCl plugin.
360 NaClManager
.prototype.sendDataToPlugin_ = function(data
) {
361 assert(this.recognizerState_
!= ManagerState_
.UNINITIALIZED
,
362 'Recognizer in uninitialized state');
363 this.plugin_
.postMessage(data
);
367 * Waits, with a timeout, for a message to be received from the plugin. If the
368 * message is not seen within the timeout, dispatch an 'error' event and go into
370 * @param {number} timeout Timeout, in milliseconds, to wait for the message.
371 * @param {!string} message Message to wait for.
374 NaClManager
.prototype.waitForMessage_ = function(timeout
, message
) {
375 assert(this.expectingMessage_
== null, 'Cannot wait for message: ' +
376 message
+ ', already waiting for message ' + this.expectingMessage_
);
379 this.recognizerState_
= ManagerState_
.ERROR
;
380 this.handleError_(Error_
.TIMEOUT
);
381 switch (this.expectingMessage_
) {
382 case hotword
.constants
.NaClPlugin
.REQUEST_MODEL
:
383 var metricValue
= UmaNaClMessageTimeout_
.REQUEST_MODEL
;
385 case hotword
.constants
.NaClPlugin
.MODEL_LOADED
:
386 var metricValue
= UmaNaClMessageTimeout_
.MODEL_LOADED
;
388 case hotword
.constants
.NaClPlugin
.READY_FOR_AUDIO
:
389 var metricValue
= UmaNaClMessageTimeout_
.READY_FOR_AUDIO
;
391 case hotword
.constants
.NaClPlugin
.STOPPED
:
392 var metricValue
= UmaNaClMessageTimeout_
.STOPPED
;
394 case hotword
.constants
.NaClPlugin
.HOTWORD_DETECTED
:
395 var metricValue
= UmaNaClMessageTimeout_
.HOTWORD_DETECTED
;
397 case hotword
.constants
.NaClPlugin
.MS_CONFIGURED
:
398 var metricValue
= UmaNaClMessageTimeout_
.MS_CONFIGURED
;
401 hotword
.metrics
.recordEnum(
402 hotword
.constants
.UmaMetrics
.NACL_MESSAGE_TIMEOUT
,
404 UmaNaClMessageTimeout_
.MAX
);
405 }.bind(this), timeout
);
406 this.expectingMessage_
= message
;
410 * Called when a message is received from the plugin. If we're waiting for that
411 * message, cancel the pending timeout.
412 * @param {string} message Message received.
415 NaClManager
.prototype.receivedMessage_ = function(message
) {
416 if (message
== this.expectingMessage_
) {
417 this.clearTimeout_();
418 this.expectingMessage_
= null;
423 * Handle a REQUEST_MODEL message from the plugin.
424 * The plugin sends this message immediately after starting.
427 NaClManager
.prototype.handleRequestModel_ = function() {
428 if (this.recognizerState_
!= ManagerState_
.LOADING
) {
431 this.logPluginLoadResult_(UmaNaClPluginLoadResult_
.SUCCESS
);
432 this.sendDataToPlugin_(
433 hotword
.constants
.NaClPlugin
.MODEL_PREFIX
+ this.modelUrl_
);
434 this.waitForMessage_(hotword
.constants
.TimeoutMs
.LONG
,
435 hotword
.constants
.NaClPlugin
.MODEL_LOADED
);
437 // Configure logging in the plugin. This can be configured any time before
438 // starting the recognizer, and now is as good a time as any.
439 if (this.loggingEnabled_
) {
440 this.sendDataToPlugin_(
441 hotword
.constants
.NaClPlugin
.LOG
+ ':' +
442 hotword
.constants
.AUDIO_LOG_SECONDS
);
445 // If the audio stream is from a hotword stream, tell the plugin.
446 if (this.hotwordStream_
) {
447 this.sendDataToPlugin_(
448 hotword
.constants
.NaClPlugin
.DSP
+ ':' +
449 hotword
.constants
.HOTWORD_STREAM_TIMEOUT_SECONDS
);
454 * Handle a MODEL_LOADED message from the plugin.
455 * The plugin sends this message after successfully loading the language model.
458 NaClManager
.prototype.handleModelLoaded_ = function() {
459 if (this.recognizerState_
!= ManagerState_
.LOADING
) {
462 this.sendDataToPlugin_(this.stream_
.getAudioTracks()[0]);
463 this.waitForMessage_(hotword
.constants
.TimeoutMs
.LONG
,
464 hotword
.constants
.NaClPlugin
.MS_CONFIGURED
);
468 * Handle a MS_CONFIGURED message from the plugin.
469 * The plugin sends this message after successfully configuring the audio input
473 NaClManager
.prototype.handleMsConfigured_ = function() {
474 if (this.recognizerState_
!= ManagerState_
.LOADING
) {
477 this.recognizerState_
= ManagerState_
.STOPPED
;
478 this.dispatchEvent(new Event(hotword
.constants
.Event
.READY
));
482 * Handle a READY_FOR_AUDIO message from the plugin.
483 * The plugin sends this message after the recognizer is started and
484 * successfully receives and processes audio data.
487 NaClManager
.prototype.handleReadyForAudio_ = function() {
488 if (this.recognizerState_
!= ManagerState_
.STARTING
) {
491 this.recognizerState_
= ManagerState_
.RUNNING
;
495 * Handle a HOTWORD_DETECTED message from the plugin.
496 * The plugin sends this message after detecting the hotword.
499 NaClManager
.prototype.handleHotwordDetected_ = function() {
500 if (this.recognizerState_
!= ManagerState_
.RUNNING
) {
503 // We'll receive a STOPPED message very soon.
504 this.recognizerState_
= ManagerState_
.STOPPING
;
505 this.waitForMessage_(hotword
.constants
.TimeoutMs
.NORMAL
,
506 hotword
.constants
.NaClPlugin
.STOPPED
);
507 var event
= new Event(hotword
.constants
.Event
.TRIGGER
);
508 event
.log
= this.preambleLog_
;
509 this.dispatchEvent(event
);
513 * Handle a STOPPED message from the plugin.
514 * This plugin sends this message after stopping the recognizer. This can happen
515 * either in response to a stop request, or after the hotword is detected.
518 NaClManager
.prototype.handleStopped_ = function() {
519 this.recognizerState_
= ManagerState_
.STOPPED
;
520 if (this.restartOnStop_
) {
521 this.restartOnStop_
= false;
522 this.startRecognizer(this.startMode_
);
527 * Handle a TIMEOUT message from the plugin.
528 * The plugin sends this message when it thinks the stream is from a DSP and
529 * a hotword wasn't detected within a timeout period after arrival of the first
533 NaClManager
.prototype.handleTimeout_ = function() {
534 if (this.recognizerState_
!= ManagerState_
.RUNNING
) {
537 this.recognizerState_
= ManagerState_
.STOPPED
;
538 this.dispatchEvent(new Event(hotword
.constants
.Event
.TIMEOUT
));
542 * Handle a SPEAKER_MODEL_SAVED message from the plugin.
543 * The plugin sends this message after writing the model to a file.
546 NaClManager
.prototype.handleSpeakerModelSaved_ = function() {
547 this.dispatchEvent(new Event(hotword
.constants
.Event
.SPEAKER_MODEL_SAVED
));
551 * Handles a message from the NaCl plugin.
552 * @param {!Event} msg Message from NaCl plugin.
555 NaClManager
.prototype.handlePluginMessage_ = function(msg
) {
557 if (typeof(msg
['data']) == 'object') {
558 // Save the preamble for delivery to the trigger handler when the trigger
560 this.preambleLog_
= msg
['data'];
563 this.receivedMessage_(msg
['data']);
564 switch (msg
['data']) {
565 case hotword
.constants
.NaClPlugin
.REQUEST_MODEL
:
566 this.handleRequestModel_();
568 case hotword
.constants
.NaClPlugin
.MODEL_LOADED
:
569 this.handleModelLoaded_();
571 case hotword
.constants
.NaClPlugin
.MS_CONFIGURED
:
572 this.handleMsConfigured_();
574 case hotword
.constants
.NaClPlugin
.READY_FOR_AUDIO
:
575 this.handleReadyForAudio_();
577 case hotword
.constants
.NaClPlugin
.HOTWORD_DETECTED
:
578 this.handleHotwordDetected_();
580 case hotword
.constants
.NaClPlugin
.STOPPED
:
581 this.handleStopped_();
583 case hotword
.constants
.NaClPlugin
.TIMEOUT
:
584 this.handleTimeout_();
586 case hotword
.constants
.NaClPlugin
.SPEAKER_MODEL_SAVED
:
587 this.handleSpeakerModelSaved_();
594 NaClManager
: NaClManager