Remove the old signature of NotificationManager::closePersistent().
[chromium-blink-merge.git] / chrome / browser / resources / hotword / nacl_manager.js
blob0d2b9b9c9fe73c72b289104b32331ddae187a18a
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() {
6 'use strict';
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  * @param {boolean} loggingEnabled Whether audio logging is enabled.
14  * @constructor
15  * @extends {cr.EventTarget}
16  */
17 function NaClManager(loggingEnabled) {
18   /**
19    * Current state of this manager.
20    * @private {hotword.NaClManager.ManagerState_}
21    */
22   this.recognizerState_ = ManagerState_.UNINITIALIZED;
24   /**
25    * The window.timeout ID associated with a pending message.
26    * @private {?number}
27    */
28   this.naclTimeoutId_ = null;
30   /**
31    * The expected message that will cancel the current timeout.
32    * @private {?string}
33    */
34   this.expectingMessage_ = null;
36   /**
37    * Whether the plugin will be started as soon as it stops.
38    * @private {boolean}
39    */
40   this.restartOnStop_ = false;
42   /**
43    * NaCl plugin element on extension background page.
44    * @private {?HTMLEmbedElement}
45    */
46   this.plugin_ = null;
48   /**
49    * URL containing hotword-model data file.
50    * @private {string}
51    */
52   this.modelUrl_ = '';
54   /**
55    * Media stream containing an audio input track.
56    * @private {?MediaStream}
57    */
58   this.stream_ = null;
60   /**
61    * The mode to start the recognizer in.
62    * @private {?chrome.hotwordPrivate.RecognizerStartMode}
63    */
64   this.startMode_ = hotword.constants.RecognizerStartMode.NORMAL;
66   /**
67    * Whether audio logging is enabled.
68    * @private {boolean}
69    */
70   this.loggingEnabled_ = loggingEnabled;
72   /**
73    * Audio log of X seconds before hotword triggered.
74    * @private {?Object}
75    */
76   this.preambleLog_ = null;
79 /**
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.
84  * @enum {number}
85  * @private
86  */
87 NaClManager.ManagerState_ = {
88   UNINITIALIZED: 0,
89   LOADING: 1,
90   STOPPING: 2,
91   STOPPED: 3,
92   STARTING: 4,
93   RUNNING: 5,
94   ERROR: 6,
95   SHUTDOWN: 7,
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
107  * @private
108  */
109 NaClManager.prototype.handleError_ = function(error) {
110   var event = new Event(hotword.constants.Event.ERROR);
111   event.data = error;
112   this.dispatchEvent(event);
116  * Record the result of loading the NaCl plugin to UMA.
117  * @param {!hotword.constants.UmaNaClPluginLoadResult} error
118  * @private
119  */
120 NaClManager.prototype.logPluginLoadResult_ = function(error) {
121   hotword.metrics.recordEnum(
122       hotword.constants.UmaMetrics.NACL_PLUGIN_LOAD_RESULT,
123       error,
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
131  * @private
132  */
133 NaClManager.prototype.setTimeout_ = function(func, timeout) {
134   assert(!this.naclTimeoutId_, 'Timeout already exists');
135   this.naclTimeoutId_ = window.setTimeout(
136       function() {
137         this.naclTimeoutId_ = null;
138         func();
139       }.bind(this), timeout);
143  * Clears the current timeout.
144  * @private
145  */
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
154  *     recognizer in.
155  */
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);
167     } else {
168       hotword.debug('Starting Recognizer in NORMAL mode');
169       this.sendDataToPlugin_(hotword.constants.NaClPlugin.RESTART);
170     }
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
177     // with that.
178   } else if (this.recognizerState_ == ManagerState_.STOPPING) {
179     // Wait until the plugin is stopped before trying to start it.
180     this.restartOnStop_ = true;
181   } else {
182     throw 'Attempting to start NaCl recogniser not in STOPPED or STOPPING ' +
183         'state';
184   }
188  * Stops the hotword recognizer.
189  */
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;
198   }
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.
207  */
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) {
213     return;
214   }
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.
222  * @private
223  */
224 NaClManager.prototype.fileExists_ = function(path) {
225   var xhr = new XMLHttpRequest();
226   xhr.open('HEAD', path, false);
227   try {
228     xhr.send();
229   } catch (err) {
230     return false;
231   }
232   if (xhr.readyState != xhr.DONE || xhr.status != 200) {
233     return false;
234   }
235   return true;
239  * Creates and returns a list of possible languages to check for hotword
240  * support.
241  * @return {!Array<string>} Array of languages.
242  * @private
243  */
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('-');
255     if (hyphen >= 0) {
256       langs.push(language.substr(0, hyphen));  // Example: 'en'.
257     }
258   }
259   langs.push('');
260   return langs;
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.
267  * @private
268  */
269 NaClManager.prototype.createPlugin_ = function(src) {
270   var plugin = /** @type {HTMLEmbedElement} */(document.createElement('embed'));
271   plugin.src = src;
272   plugin.type = 'application/x-nacl';
273   document.body.appendChild(plugin);
274   return 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.
282  */
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_();
288   var i, j;
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_' +
296         langs[i] + '.nmf';
297     var dataExists = this.fileExists_(dataSrc) && this.fileExists_(pluginSrc);
298     if (!dataExists) {
299       continue;
300     }
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;
307       return false;
308     }
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),
315                             false);
317     plugin.addEventListener('crash',
318                             function() {
319                               this.handleError_(Error_.NACL_CRASH);
320                               this.logPluginLoadResult_(
321                                   UmaNaClPluginLoadResult_.CRASH);
322                             }.bind(this),
323                             false);
324     return true;
325   }
326   this.recognizerState_ = ManagerState_.ERROR;
327   this.logPluginLoadResult_(UmaNaClPluginLoadResult_.NO_MODULE_FOUND);
328   return false;
332  * Shuts down the NaCl plugin and frees all resources.
333  */
334 NaClManager.prototype.shutdown = function() {
335   if (this.plugin_ != null) {
336     document.body.removeChild(this.plugin_);
337     this.plugin_ = null;
338   }
339   this.clearTimeout_();
340   this.recognizerState_ = ManagerState_.SHUTDOWN;
341   if (this.stream_)
342     this.stream_.stop();
343   this.stream_ = null;
347  * Sends data to the NaCl plugin.
348  * @param {!string|!MediaStreamTrack} data Command to be sent to NaCl plugin.
349  * @private
350  */
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
360  * the ERROR state.
361  * @param {number} timeout Timeout, in milliseconds, to wait for the message.
362  * @param {!string} message Message to wait for.
363  * @private
364  */
365 NaClManager.prototype.waitForMessage_ = function(timeout, message) {
366   assert(this.expectingMessage_ == null, 'Cannot wait for message: ' +
367       message + ', already waiting for message ' + this.expectingMessage_);
368   this.setTimeout_(
369       function() {
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;
375             break;
376           case hotword.constants.NaClPlugin.MODEL_LOADED:
377             var metricValue = UmaNaClMessageTimeout_.MODEL_LOADED;
378             break;
379           case hotword.constants.NaClPlugin.READY_FOR_AUDIO:
380             var metricValue = UmaNaClMessageTimeout_.READY_FOR_AUDIO;
381             break;
382           case hotword.constants.NaClPlugin.STOPPED:
383             var metricValue = UmaNaClMessageTimeout_.STOPPED;
384             break;
385           case hotword.constants.NaClPlugin.HOTWORD_DETECTED:
386             var metricValue = UmaNaClMessageTimeout_.HOTWORD_DETECTED;
387             break;
388           case hotword.constants.NaClPlugin.MS_CONFIGURED:
389             var metricValue = UmaNaClMessageTimeout_.MS_CONFIGURED;
390             break;
391         }
392         hotword.metrics.recordEnum(
393             hotword.constants.UmaMetrics.NACL_MESSAGE_TIMEOUT,
394             metricValue,
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.
404  * @private
405  */
406 NaClManager.prototype.receivedMessage_ = function(message) {
407   if (message == this.expectingMessage_) {
408     this.clearTimeout_();
409     this.expectingMessage_ = null;
410   }
414  * Handle a REQUEST_MODEL message from the plugin.
415  * The plugin sends this message immediately after starting.
416  * @private
417  */
418 NaClManager.prototype.handleRequestModel_ = function() {
419   if (this.recognizerState_ != ManagerState_.LOADING) {
420     return;
421   }
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);
434   }
438  * Handle a MODEL_LOADED message from the plugin.
439  * The plugin sends this message after successfully loading the language model.
440  * @private
441  */
442 NaClManager.prototype.handleModelLoaded_ = function() {
443   if (this.recognizerState_ != ManagerState_.LOADING) {
444     return;
445   }
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
454  * stream.
455  * @private
456  */
457 NaClManager.prototype.handleMsConfigured_ = function() {
458   if (this.recognizerState_ != ManagerState_.LOADING) {
459     return;
460   }
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.
469  * @private
470  */
471 NaClManager.prototype.handleReadyForAudio_ = function() {
472   if (this.recognizerState_ != ManagerState_.STARTING) {
473     return;
474   }
475   this.recognizerState_ = ManagerState_.RUNNING;
479  * Handle a HOTWORD_DETECTED message from the plugin.
480  * The plugin sends this message after detecting the hotword.
481  * @private
482  */
483 NaClManager.prototype.handleHotwordDetected_ = function() {
484   if (this.recognizerState_ != ManagerState_.RUNNING) {
485     return;
486   }
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.
500  * @private
501  */
502 NaClManager.prototype.handleStopped_ = function() {
503   this.recognizerState_ = ManagerState_.STOPPED;
504   if (this.restartOnStop_) {
505     this.restartOnStop_ = false;
506     this.startRecognizer(this.startMode_);
507   }
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
514  * audio samples.
515  * @private
516  */
517 NaClManager.prototype.handleTimeout_ = function() {
518   if (this.recognizerState_ != ManagerState_.RUNNING) {
519     return;
520   }
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.
528  * @private
529  */
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.
537  * @private
538  */
539 NaClManager.prototype.handlePluginMessage_ = function(msg) {
540   if (msg['data']) {
541     if (typeof(msg['data']) == 'object') {
542       // Save the preamble for delivery to the trigger handler when the trigger
543       // message arrives.
544       this.preambleLog_ = msg['data'];
545       return;
546     }
547     this.receivedMessage_(msg['data']);
548     switch (msg['data']) {
549       case hotword.constants.NaClPlugin.REQUEST_MODEL:
550         this.handleRequestModel_();
551         break;
552       case hotword.constants.NaClPlugin.MODEL_LOADED:
553         this.handleModelLoaded_();
554         break;
555       case hotword.constants.NaClPlugin.MS_CONFIGURED:
556         this.handleMsConfigured_();
557         break;
558       case hotword.constants.NaClPlugin.READY_FOR_AUDIO:
559         this.handleReadyForAudio_();
560         break;
561       case hotword.constants.NaClPlugin.HOTWORD_DETECTED:
562         this.handleHotwordDetected_();
563         break;
564       case hotword.constants.NaClPlugin.STOPPED:
565         this.handleStopped_();
566         break;
567       case hotword.constants.NaClPlugin.TIMEOUT:
568         this.handleTimeout_();
569         break;
570       case hotword.constants.NaClPlugin.SPEAKER_MODEL_SAVED:
571         this.handleSpeakerModelSaved_();
572         break;
573     }
574   }
577 return {
578   NaClManager: NaClManager