Update To 11.40.268.0
[platform/framework/web/crosswalk.git] / src / chrome / browser / resources / hotword / nacl_manager.js
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.
4
5 cr.define('hotword', function() {
6 'use strict';
7
8 /**
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
11  * shutdown.
12  *
13  * @constructor
14  * @extends {cr.EventTarget}
15  */
16 function NaClManager() {
17   /**
18    * Current state of this manager.
19    * @private {hotword.NaClManager.ManagerState_}
20    */
21   this.recognizerState_ = ManagerState_.UNINITIALIZED;
22
23   /**
24    * The window.timeout ID associated with a pending message.
25    * @private {?number}
26    */
27   this.naclTimeoutId_ = null;
28
29   /**
30    * The expected message that will cancel the current timeout.
31    * @private {?string}
32    */
33   this.expectingMessage_ = null;
34
35   /**
36    * Whether the plugin will be started as soon as it stops.
37    * @private {boolean}
38    */
39   this.restartOnStop_ = false;
40
41   /**
42    * NaCl plugin element on extension background page.
43    * @private {?HTMLEmbedElement}
44    */
45   this.plugin_ = null;
46
47   /**
48    * URL containing hotword-model data file.
49    * @private {string}
50    */
51   this.modelUrl_ = '';
52
53   /**
54    * Media stream containing an audio input track.
55    * @private {?MediaStream}
56    */
57   this.stream_ = null;
58 };
59
60 /**
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.
65  * @enum {number}
66  * @private
67  */
68 NaClManager.ManagerState_ = {
69   UNINITIALIZED: 0,
70   LOADING: 1,
71   STOPPING: 2,
72   STOPPED: 3,
73   STARTING: 4,
74   RUNNING: 5,
75   ERROR: 6,
76   SHUTDOWN: 7,
77 };
78 var ManagerState_ = NaClManager.ManagerState_;
79 var Error_ = hotword.constants.Error;
80 var UmaNaClMessageTimeout_ = hotword.constants.UmaNaClMessageTimeout;
81 var UmaNaClPluginLoadResult_ = hotword.constants.UmaNaClPluginLoadResult;
82
83 NaClManager.prototype.__proto__ = cr.EventTarget.prototype;
84
85 /**
86  * Called when an error occurs. Dispatches an event.
87  * @param {!hotword.constants.Error} error
88  * @private
89  */
90 NaClManager.prototype.handleError_ = function(error) {
91   var event = new Event(hotword.constants.Event.ERROR);
92   event.data = error;
93   this.dispatchEvent(event);
94 };
95
96 /**
97  * Record the result of loading the NaCl plugin to UMA.
98  * @param {!hotword.constants.UmaNaClPluginLoadResult} error
99  * @private
100  */
101 NaClManager.prototype.logPluginLoadResult_ = function(error) {
102   hotword.metrics.recordEnum(
103       hotword.constants.UmaMetrics.NACL_PLUGIN_LOAD_RESULT,
104       error,
105       UmaNaClPluginLoadResult_.MAX);
106 };
107
108 /**
109  * @return {boolean} True if the recognizer is in a running state.
110  */
111 NaClManager.prototype.isRunning = function() {
112   return this.recognizerState_ == ManagerState_.RUNNING;
113 };
114
115 /**
116  * Set a timeout. Only allow one timeout to exist at any given time.
117  * @param {!function()} func
118  * @param {number} timeout
119  * @private
120  */
121 NaClManager.prototype.setTimeout_ = function(func, timeout) {
122   assert(!this.naclTimeoutId_, 'Timeout already exists');
123   this.naclTimeoutId_ = window.setTimeout(
124       function() {
125         this.naclTimeoutId_ = null;
126         func();
127       }.bind(this), timeout);
128 };
129
130 /**
131  * Clears the current timeout.
132  * @private
133  */
134 NaClManager.prototype.clearTimeout_ = function() {
135   window.clearTimeout(this.naclTimeoutId_);
136   this.naclTimeoutId_ = null;
137 };
138
139 /**
140  * Starts a stopped or stopping hotword recognizer (NaCl plugin).
141  */
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;
151   } else {
152     throw 'Attempting to start NaCl recogniser not in STOPPED or STOPPING ' +
153         'state';
154   }
155 };
156
157 /**
158  * Stops the hotword recognizer.
159  */
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);
165 };
166
167 /**
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.
171  * @private
172  */
173 NaClManager.prototype.fileExists_ = function(path) {
174   var xhr = new XMLHttpRequest();
175   xhr.open('HEAD', path, false);
176   try {
177     xhr.send();
178   } catch (err) {
179     return false;
180   }
181   if (xhr.readyState != xhr.DONE || xhr.status != 200) {
182     return false;
183   }
184   return true;
185 };
186
187 /**
188  * Creates and returns a list of possible languages to check for hotword
189  * support.
190  * @return {!Array.<string>} Array of languages.
191  * @private
192  */
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('-');
204     if (hyphen >= 0) {
205       langs.push(language.substr(0, hyphen));  // Example: 'en'.
206     }
207   }
208   langs.push('');
209   return langs;
210 };
211
212 /**
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.
216  * @private
217  */
218 NaClManager.prototype.createPlugin_ = function(src) {
219   var plugin = /** @type {HTMLEmbedElement} */(document.createElement('embed'));
220   plugin.src = src;
221   plugin.type = 'application/x-nacl';
222   document.body.appendChild(plugin);
223   return plugin;
224 };
225
226 /**
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.
231  */
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_();
237   var i, j;
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_' +
245         langs[i] + '.nmf';
246     var dataExists = this.fileExists_(dataSrc) && this.fileExists_(pluginSrc);
247     if (!dataExists) {
248       continue;
249     }
250
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;
256       return false;
257     }
258     this.modelUrl_ = chrome.extension.getURL(dataSrc);
259     this.stream_ = stream;
260     this.recognizerState_ = ManagerState_.LOADING;
261
262     plugin.addEventListener('message',
263                             this.handlePluginMessage_.bind(this),
264                             false);
265
266     plugin.addEventListener('crash',
267                             function() {
268                               this.handleError_(Error_.NACL_CRASH);
269                               this.logPluginLoadResult_(
270                                   UmaNaClPluginLoadResult_.CRASH);
271                             }.bind(this),
272                             false);
273     return true;
274   }
275   this.recognizerState_ = ManagerState_.ERROR;
276   this.logPluginLoadResult_(UmaNaClPluginLoadResult_.NO_MODULE_FOUND);
277   return false;
278 };
279
280 /**
281  * Shuts down the NaCl plugin and frees all resources.
282  */
283 NaClManager.prototype.shutdown = function() {
284   if (this.plugin_ != null) {
285     document.body.removeChild(this.plugin_);
286     this.plugin_ = null;
287   }
288   this.clearTimeout_();
289   this.recognizerState_ = ManagerState_.SHUTDOWN;
290   this.stream_ = null;
291 };
292
293 /**
294  * Sends data to the NaCl plugin.
295  * @param {!string|!MediaStreamTrack} data Command to be sent to NaCl plugin.
296  * @private
297  */
298 NaClManager.prototype.sendDataToPlugin_ = function(data) {
299   assert(this.recognizerState_ != ManagerState_.UNINITIALIZED,
300          'Recognizer in uninitialized state');
301   this.plugin_.postMessage(data);
302 };
303
304 /**
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
307  * the ERROR state.
308  * @param {number} timeout Timeout, in milliseconds, to wait for the message.
309  * @param {!string} message Message to wait for.
310  * @private
311  */
312 NaClManager.prototype.waitForMessage_ = function(timeout, message) {
313   assert(this.expectingMessage_ == null,
314          'Already waiting for message ' + this.expectingMessage_);
315   this.setTimeout_(
316       function() {
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;
322             break;
323           case hotword.constants.NaClPlugin.MODEL_LOADED:
324             var metricValue = UmaNaClMessageTimeout_.MODEL_LOADED;
325             break;
326           case hotword.constants.NaClPlugin.READY_FOR_AUDIO:
327             var metricValue = UmaNaClMessageTimeout_.READY_FOR_AUDIO;
328             break;
329           case hotword.constants.NaClPlugin.STOPPED:
330             var metricValue = UmaNaClMessageTimeout_.STOPPED;
331             break;
332           case hotword.constants.NaClPlugin.HOTWORD_DETECTED:
333             var metricValue = UmaNaClMessageTimeout_.HOTWORD_DETECTED;
334             break;
335           case hotword.constants.NaClPlugin.MS_CONFIGURED:
336             var metricValue = UmaNaClMessageTimeout_.MS_CONFIGURED;
337             break;
338         }
339         hotword.metrics.recordEnum(
340             hotword.constants.UmaMetrics.NACL_MESSAGE_TIMEOUT,
341             metricValue,
342             UmaNaClMessageTimeout_.MAX);
343       }.bind(this), timeout);
344   this.expectingMessage_ = message;
345 };
346
347 /**
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.
351  * @private
352  */
353 NaClManager.prototype.receivedMessage_ = function(message) {
354   if (message == this.expectingMessage_) {
355     this.clearTimeout_();
356     this.expectingMessage_ = null;
357   }
358 };
359
360 /**
361  * Handle a REQUEST_MODEL message from the plugin.
362  * The plugin sends this message immediately after starting.
363  * @private
364  */
365 NaClManager.prototype.handleRequestModel_ = function() {
366   if (this.recognizerState_ != ManagerState_.LOADING) {
367     return;
368   }
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);
374 };
375
376 /**
377  * Handle a MODEL_LOADED message from the plugin.
378  * The plugin sends this message after successfully loading the language model.
379  * @private
380  */
381 NaClManager.prototype.handleModelLoaded_ = function() {
382   if (this.recognizerState_ != ManagerState_.LOADING) {
383     return;
384   }
385   this.sendDataToPlugin_(this.stream_.getAudioTracks()[0]);
386   this.waitForMessage_(hotword.constants.TimeoutMs.LONG,
387                        hotword.constants.NaClPlugin.MS_CONFIGURED);
388 };
389
390 /**
391  * Handle a MS_CONFIGURED message from the plugin.
392  * The plugin sends this message after successfully configuring the audio input
393  * stream.
394  * @private
395  */
396 NaClManager.prototype.handleMsConfigured_ = function() {
397   if (this.recognizerState_ != ManagerState_.LOADING) {
398     return;
399   }
400   this.recognizerState_ = ManagerState_.STOPPED;
401   this.dispatchEvent(new Event(hotword.constants.Event.READY));
402 };
403
404 /**
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.
408  * @private
409  */
410 NaClManager.prototype.handleReadyForAudio_ = function() {
411   if (this.recognizerState_ != ManagerState_.STARTING) {
412     return;
413   }
414   this.recognizerState_ = ManagerState_.RUNNING;
415 };
416
417 /**
418  * Handle a HOTWORD_DETECTED message from the plugin.
419  * The plugin sends this message after detecting the hotword.
420  * @private
421  */
422 NaClManager.prototype.handleHotwordDetected_ = function() {
423   if (this.recognizerState_ != ManagerState_.RUNNING) {
424     return;
425   }
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));
431 };
432
433 /**
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.
437  * @private
438  */
439 NaClManager.prototype.handleStopped_ = function() {
440   this.recognizerState_ = ManagerState_.STOPPED;
441   if (this.restartOnStop_) {
442     this.restartOnStop_ = false;
443     this.startRecognizer();
444   }
445 };
446
447 /**
448  * Handles a message from the NaCl plugin.
449  * @param {!Event} msg Message from NaCl plugin.
450  * @private
451  */
452 NaClManager.prototype.handlePluginMessage_ = function(msg) {
453   if (msg['data']) {
454     this.receivedMessage_(msg['data']);
455     switch (msg['data']) {
456       case hotword.constants.NaClPlugin.REQUEST_MODEL:
457         this.handleRequestModel_();
458         break;
459       case hotword.constants.NaClPlugin.MODEL_LOADED:
460         this.handleModelLoaded_();
461         break;
462       case hotword.constants.NaClPlugin.MS_CONFIGURED:
463         this.handleMsConfigured_();
464         break;
465       case hotword.constants.NaClPlugin.READY_FOR_AUDIO:
466         this.handleReadyForAudio_();
467         break;
468       case hotword.constants.NaClPlugin.HOTWORD_DETECTED:
469         this.handleHotwordDetected_();
470         break;
471       case hotword.constants.NaClPlugin.STOPPED:
472         this.handleStopped_();
473         break;
474     }
475   }
476 };
477
478 return {
479   NaClManager: NaClManager
480 };
481
482 });