1 // Copyright (c) 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.
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
;
81 NaClManager
.prototype.__proto__
= cr
.EventTarget
.prototype;
84 * Called when an error occurs. Dispatches an event.
85 * @param {!hotword.constants.Error} error
88 NaClManager
.prototype.handleError_ = function(error
) {
89 event
= new Event(hotword
.constants
.Event
.ERROR
);
91 this.dispatchEvent(event
);
95 * @return {boolean} True if the recognizer is in a running state.
97 NaClManager
.prototype.isRunning = function() {
98 return this.recognizerState_
== ManagerState_
.RUNNING
;
102 * Set a timeout. Only allow one timeout to exist at any given time.
103 * @param {!function()} func
104 * @param {number} timeout
107 NaClManager
.prototype.setTimeout_ = function(func
, timeout
) {
108 assert(!this.naclTimeoutId_
);
109 this.naclTimeoutId_
= window
.setTimeout(
111 this.naclTimeoutId_
= null;
113 }.bind(this), timeout
);
117 * Clears the current timeout.
120 NaClManager
.prototype.clearTimeout_ = function() {
121 window
.clearTimeout(this.naclTimeoutId_
);
122 this.naclTimeoutId_
= null;
126 * Starts a stopped or stopping hotword recognizer (NaCl plugin).
128 NaClManager
.prototype.startRecognizer = function() {
129 if (this.recognizerState_
== ManagerState_
.STOPPED
) {
130 this.recognizerState_
= ManagerState_
.STARTING
;
131 this.sendDataToPlugin_(hotword
.constants
.NaClPlugin
.RESTART
);
132 this.waitForMessage_(hotword
.constants
.TimeoutMs
.LONG
,
133 hotword
.constants
.NaClPlugin
.READY_FOR_AUDIO
);
134 } else if (this.recognizerState_
== ManagerState_
.STOPPING
) {
135 // Wait until the plugin is stopped before trying to start it.
136 this.restartOnStop_
= true;
138 throw 'Attempting to start NaCl recogniser not in STOPPED or STOPPING ' +
144 * Stops the hotword recognizer.
146 NaClManager
.prototype.stopRecognizer = function() {
147 this.sendDataToPlugin_(hotword
.constants
.NaClPlugin
.STOP
);
148 this.recognizerState_
= ManagerState_
.STOPPING
;
149 this.waitForMessage_(hotword
.constants
.TimeoutMs
.NORMAL
,
150 hotword
.constants
.NaClPlugin
.STOPPED
);
154 * Checks whether the file at the given path exists.
155 * @param {!string} path Path to a file. Can be any valid URL.
156 * @return {boolean} True if the patch exists.
159 NaClManager
.prototype.fileExists_ = function(path
) {
160 var xhr
= new XMLHttpRequest();
161 xhr
.open('HEAD', path
, false);
167 if (xhr
.readyState
!= xhr
.DONE
|| xhr
.status
!= 200) {
174 * Creates and returns a list of possible languages to check for hotword
176 * @return {!Array.<string>} Array of languages.
179 NaClManager
.prototype.getPossibleLanguages_ = function() {
180 // Create array used to search first for language-country, if not found then
181 // search for language, if not found then no language (empty string).
182 // For example, search for 'en-us', then 'en', then ''.
183 var langs
= new Array();
184 if (hotword
.constants
.UI_LANGUAGE
) {
185 // Chrome webstore doesn't support uppercase path: crbug.com/353407
186 var language
= hotword
.constants
.UI_LANGUAGE
.toLowerCase();
187 langs
.push(language
); // Example: 'en-us'.
188 // Remove country to add just the language to array.
189 var hyphen
= language
.lastIndexOf('-');
191 langs
.push(language
.substr(0, hyphen
)); // Example: 'en'.
199 * Creates a NaCl plugin object and attaches it to the page.
200 * @param {!string} src Location of the plugin.
201 * @return {!Nacl} NaCl plugin DOM object.
204 NaClManager
.prototype.createPlugin_ = function(src
) {
205 var plugin
= document
.createElement('embed');
207 plugin
.type
= 'application/x-nacl';
208 document
.body
.appendChild(plugin
);
213 * Initializes the NaCl manager.
214 * @param {!string} naclArch Either 'arm', 'x86-32' or 'x86-64'.
215 * @param {!MediaStream} stream A stream containing an audio source track.
216 * @return {boolean} True if the successful.
218 NaClManager
.prototype.initialize = function(naclArch
, stream
) {
219 assert(this.recognizerState_
== ManagerState_
.UNINITIALIZED
);
220 var langs
= this.getPossibleLanguages_();
222 // For country-lang variations. For example, when combined with path it will
223 // attempt to find: '/x86-32_en-gb/', else '/x86-32_en/', else '/x86-32_/'.
224 for (i
= 0; i
< langs
.length
; i
++) {
225 var folder
= hotword
.constants
.SHARED_MODULE_ROOT
+ '/_platform_specific/' +
226 naclArch
+ '_' + langs
[i
] + '/';
227 var dataSrc
= folder
+ hotword
.constants
.File
.RECOGNIZER_CONFIG
;
228 var pluginSrc
= hotword
.constants
.SHARED_MODULE_ROOT
+ '/hotword_' +
230 var dataExists
= this.fileExists_(dataSrc
) && this.fileExists_(pluginSrc
);
235 var plugin
= this.createPlugin_(pluginSrc
);
236 this.plugin_
= /** @type {Nacl} */ (plugin
);
237 if (!this.plugin_
|| !this.plugin_
.postMessage
) {
238 document
.body
.removeChild(this.plugin_
);
239 this.recognizerState_
= ManagerState_
.ERROR
;
242 this.modelUrl_
= chrome
.extension
.getURL(dataSrc
);
243 this.stream_
= stream
;
244 this.recognizerState_
= ManagerState_
.LOADING
;
246 plugin
.addEventListener('message',
247 this.handlePluginMessage_
.bind(this),
250 plugin
.addEventListener('crash',
251 this.handleError_
.bind(this, Error_
.NACL_CRASH
),
255 this.recognizerState_
= ManagerState_
.ERROR
;
260 * Shuts down the NaCl plugin and frees all resources.
262 NaClManager
.prototype.shutdown = function() {
263 if (this.plugin_
!= null) {
264 document
.body
.removeChild(this.plugin_
);
267 this.clearTimeout_();
268 this.recognizerState_
= ManagerState_
.SHUTDOWN
;
273 * Sends data to the NaCl plugin.
274 * @param {!string} data Command to be sent to NaCl plugin.
277 NaClManager
.prototype.sendDataToPlugin_ = function(data
) {
278 assert(this.recognizerState_
!= ManagerState_
.UNINITIALIZED
);
279 this.plugin_
.postMessage(data
);
283 * Waits, with a timeout, for a message to be received from the plugin. If the
284 * message is not seen within the timeout, dispatch an 'error' event and go into
286 * @param {number} timeout Timeout, in milliseconds, to wait for the message.
287 * @param {!string} message Message to wait for.
290 NaClManager
.prototype.waitForMessage_ = function(timeout
, message
) {
291 assert(this.expectingMessage_
== null,
292 'Already waiting for message ' + this.expectingMessage_
);
295 this.recognizerState_
= ManagerState_
.ERROR
;
296 this.handleError_(Error_
.TIMEOUT
);
297 }.bind(this), timeout
);
298 this.expectingMessage_
= message
;
302 * Called when a message is received from the plugin. If we're waiting for that
303 * message, cancel the pending timeout.
304 * @param {string} message Message received.
307 NaClManager
.prototype.receivedMessage_ = function(message
) {
308 if (message
== this.expectingMessage_
) {
309 this.clearTimeout_();
310 this.expectingMessage_
= null;
315 * Handle a REQUEST_MODEL message from the plugin.
316 * The plugin sends this message immediately after starting.
319 NaClManager
.prototype.handleRequestModel_ = function() {
320 if (this.recognizerState_
!= ManagerState_
.LOADING
) {
323 this.sendDataToPlugin_(
324 hotword
.constants
.NaClPlugin
.MODEL_PREFIX
+ this.modelUrl_
);
325 this.waitForMessage_(hotword
.constants
.TimeoutMs
.LONG
,
326 hotword
.constants
.NaClPlugin
.MODEL_LOADED
);
330 * Handle a MODEL_LOADED message from the plugin.
331 * The plugin sends this message after successfully loading the language model.
334 NaClManager
.prototype.handleModelLoaded_ = function() {
335 if (this.recognizerState_
!= ManagerState_
.LOADING
) {
338 this.sendDataToPlugin_(this.stream_
.getAudioTracks()[0]);
339 this.waitForMessage_(hotword
.constants
.TimeoutMs
.LONG
,
340 hotword
.constants
.NaClPlugin
.MS_CONFIGURED
);
344 * Handle a MS_CONFIGURED message from the plugin.
345 * The plugin sends this message after successfully configuring the audio input
349 NaClManager
.prototype.handleMsConfigured_ = function() {
350 if (this.recognizerState_
!= ManagerState_
.LOADING
) {
353 this.recognizerState_
= ManagerState_
.STOPPED
;
354 this.dispatchEvent(new Event(hotword
.constants
.Event
.READY
));
358 * Handle a READY_FOR_AUDIO message from the plugin.
359 * The plugin sends this message after the recognizer is started and
360 * successfully receives and processes audio data.
363 NaClManager
.prototype.handleReadyForAudio_ = function() {
364 if (this.recognizerState_
!= ManagerState_
.STARTING
) {
367 this.recognizerState_
= ManagerState_
.RUNNING
;
371 * Handle a HOTWORD_DETECTED message from the plugin.
372 * The plugin sends this message after detecting the hotword.
375 NaClManager
.prototype.handleHotwordDetected_ = function() {
376 if (this.recognizerState_
!= ManagerState_
.RUNNING
) {
379 // We'll receive a STOPPED message very soon.
380 this.recognizerState_
= ManagerState_
.STOPPING
;
381 this.waitForMessage_(hotword
.constants
.TimeoutMs
.NORMAL
,
382 hotword
.constants
.NaClPlugin
.STOPPED
);
383 this.dispatchEvent(new Event(hotword
.constants
.Event
.TRIGGER
));
387 * Handle a STOPPED message from the plugin.
388 * This plugin sends this message after stopping the recognizer. This can happen
389 * either in response to a stop request, or after the hotword is detected.
392 NaClManager
.prototype.handleStopped_ = function() {
393 this.recognizerState_
= ManagerState_
.STOPPED
;
394 if (this.restartOnStop_
) {
395 this.restartOnStop_
= false;
396 this.startRecognizer();
401 * Handles a message from the NaCl plugin.
402 * @param {!Event} msg Message from NaCl plugin.
405 NaClManager
.prototype.handlePluginMessage_ = function(msg
) {
407 this.receivedMessage_(msg
['data']);
408 switch (msg
['data']) {
409 case hotword
.constants
.NaClPlugin
.REQUEST_MODEL
:
410 this.handleRequestModel_();
412 case hotword
.constants
.NaClPlugin
.MODEL_LOADED
:
413 this.handleModelLoaded_();
415 case hotword
.constants
.NaClPlugin
.MS_CONFIGURED
:
416 this.handleMsConfigured_();
418 case hotword
.constants
.NaClPlugin
.READY_FOR_AUDIO
:
419 this.handleReadyForAudio_();
421 case hotword
.constants
.NaClPlugin
.HOTWORD_DETECTED
:
422 this.handleHotwordDetected_();
424 case hotword
.constants
.NaClPlugin
.STOPPED
:
425 this.handleStopped_();
432 NaClManager
: NaClManager