Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(281)

Side by Side Diff: chrome/browser/resources/hotword/nacl_manager.js

Issue 464213002: Add a 'NaCl manager' to manage the state of the NaCl hotword detector plugin. (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: Rebase. Created 6 years, 4 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « chrome/browser/resources/hotword/manifest.json ('k') | no next file » | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(Empty)
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.
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 {?Nacl}
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
81 NaClManager.prototype.__proto__ = cr.EventTarget.prototype;
82
83 /**
84 * Called when an error occurs. Dispatches an event.
85 * @param {!hotword.constants.Error} error
86 * @private
87 */
88 NaClManager.prototype.handleError_ = function(error) {
89 event = new Event(hotword.constants.Event.ERROR);
90 event.data = error;
91 this.dispatchEvent(event);
92 };
93
94 /**
95 * @return {boolean} True if the recognizer is in a running state.
96 */
97 NaClManager.prototype.isRunning = function() {
98 return this.recognizerState_ == ManagerState_.RUNNING;
99 };
100
101 /**
102 * Set a timeout. Only allow one timeout to exist at any given time.
103 * @param {!function()} func
104 * @param {number} timeout
105 * @private
106 */
107 NaClManager.prototype.setTimeout_ = function(func, timeout) {
108 assert(!this.naclTimeoutId_);
109 this.naclTimeoutId_ = window.setTimeout(
110 function() {
111 this.naclTimeoutId_ = null;
112 func();
113 }.bind(this), timeout);
114 };
115
116 /**
117 * Clears the current timeout.
118 * @private
119 */
120 NaClManager.prototype.clearTimeout_ = function() {
121 window.clearTimeout(this.naclTimeoutId_);
122 this.naclTimeoutId_ = null;
123 };
124
125 /**
126 * Starts a stopped or stopping hotword recognizer (NaCl plugin).
127 */
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;
137 } else {
138 throw 'Attempting to start NaCl recogniser not in STOPPED or STOPPING ' +
139 'state';
140 }
141 };
142
143 /**
144 * Stops the hotword recognizer.
145 */
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);
151 };
152
153 /**
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.
157 * @private
158 */
159 NaClManager.prototype.fileExists_ = function(path) {
160 var xhr = new XMLHttpRequest();
161 xhr.open('HEAD', path, false);
162 try {
163 xhr.send();
164 } catch (err) {
165 return false;
166 }
167 if (xhr.readyState != xhr.DONE || xhr.status != 200) {
168 return false;
169 }
170 return true;
171 };
172
173 /**
174 * Creates and returns a list of possible languages to check for hotword
175 * support.
176 * @return {!Array.<string>} Array of languages.
177 * @private
178 */
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('-');
190 if (hyphen >= 0) {
191 langs.push(language.substr(0, hyphen)); // Example: 'en'.
192 }
193 }
194 langs.push('');
195 return langs;
196 };
197
198 /**
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.
202 * @private
203 */
204 NaClManager.prototype.createPlugin_ = function(src) {
205 var plugin = document.createElement('embed');
206 plugin.src = src;
207 plugin.type = 'application/x-nacl';
208 document.body.appendChild(plugin);
209 return plugin;
210 };
211
212 /**
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.
217 */
218 NaClManager.prototype.initialize = function(naclArch, stream) {
219 assert(this.recognizerState_ == ManagerState_.UNINITIALIZED);
220 var langs = this.getPossibleLanguages_();
221 var i, j;
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_' +
229 langs[i] + '.nmf';
230 var dataExists = this.fileExists_(dataSrc) && this.fileExists_(pluginSrc);
231 if (!dataExists) {
232 continue;
233 }
234
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;
240 return false;
241 }
242 this.modelUrl_ = chrome.extension.getURL(dataSrc);
243 this.stream_ = stream;
244 this.recognizerState_ = ManagerState_.LOADING;
245
246 plugin.addEventListener('message',
247 this.handlePluginMessage_.bind(this),
248 false);
249
250 plugin.addEventListener('crash',
251 this.handleError_.bind(this, Error_.NACL_CRASH),
252 false);
253 return true;
254 }
255 this.recognizerState_ = ManagerState_.ERROR;
256 return false;
257 };
258
259 /**
260 * Shuts down the NaCl plugin and frees all resources.
261 */
262 NaClManager.prototype.shutdown = function() {
263 if (this.plugin_ != null) {
264 document.body.removeChild(this.plugin_);
265 this.plugin_ = null;
266 }
267 this.clearTimeout_();
268 this.recognizerState_ = ManagerState_.SHUTDOWN;
269 this.stream_ = null;
270 };
271
272 /**
273 * Sends data to the NaCl plugin.
274 * @param {!string} data Command to be sent to NaCl plugin.
275 * @private
276 */
277 NaClManager.prototype.sendDataToPlugin_ = function(data) {
278 assert(this.recognizerState_ != ManagerState_.UNINITIALIZED);
279 this.plugin_.postMessage(data);
280 };
281
282 /**
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
285 * the ERROR state.
286 * @param {number} timeout Timeout, in milliseconds, to wait for the message.
287 * @param {!string} message Message to wait for.
288 * @private
289 */
290 NaClManager.prototype.waitForMessage_ = function(timeout, message) {
291 assert(this.expectingMessage_ == null,
292 'Already waiting for message ' + this.expectingMessage_);
293 this.setTimeout_(
294 function() {
295 this.recognizerState_ = ManagerState_.ERROR;
296 this.handleError_(Error_.TIMEOUT);
297 }.bind(this), timeout);
298 this.expectingMessage_ = message;
299 };
300
301 /**
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.
305 * @private
306 */
307 NaClManager.prototype.receivedMessage_ = function(message) {
308 if (message == this.expectingMessage_) {
309 this.clearTimeout_();
310 this.expectingMessage_ = null;
311 }
312 };
313
314 /**
315 * Handle a REQUEST_MODEL message from the plugin.
316 * The plugin sends this message immediately after starting.
317 * @private
318 */
319 NaClManager.prototype.handleRequestModel_ = function() {
320 if (this.recognizerState_ != ManagerState_.LOADING) {
321 return;
322 }
323 this.sendDataToPlugin_(
324 hotword.constants.NaClPlugin.MODEL_PREFIX + this.modelUrl_);
325 this.waitForMessage_(hotword.constants.TimeoutMs.LONG,
326 hotword.constants.NaClPlugin.MODEL_LOADED);
327 };
328
329 /**
330 * Handle a MODEL_LOADED message from the plugin.
331 * The plugin sends this message after successfully loading the language model.
332 * @private
333 */
334 NaClManager.prototype.handleModelLoaded_ = function() {
335 if (this.recognizerState_ != ManagerState_.LOADING) {
336 return;
337 }
338 this.sendDataToPlugin_(this.stream_.getAudioTracks()[0]);
339 this.waitForMessage_(hotword.constants.TimeoutMs.LONG,
340 hotword.constants.NaClPlugin.MS_CONFIGURED);
341 };
342
343 /**
344 * Handle a MS_CONFIGURED message from the plugin.
345 * The plugin sends this message after successfully configuring the audio input
346 * stream.
347 * @private
348 */
349 NaClManager.prototype.handleMsConfigured_ = function() {
350 if (this.recognizerState_ != ManagerState_.LOADING) {
351 return;
352 }
353 this.recognizerState_ = ManagerState_.STOPPED;
354 this.dispatchEvent(new Event(hotword.constants.Event.READY));
355 };
356
357 /**
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.
361 * @private
362 */
363 NaClManager.prototype.handleReadyForAudio_ = function() {
364 if (this.recognizerState_ != ManagerState_.STARTING) {
365 return;
366 }
367 this.recognizerState_ = ManagerState_.RUNNING;
368 };
369
370 /**
371 * Handle a HOTWORD_DETECTED message from the plugin.
372 * The plugin sends this message after detecting the hotword.
373 * @private
374 */
375 NaClManager.prototype.handleHotwordDetected_ = function() {
376 if (this.recognizerState_ != ManagerState_.RUNNING) {
377 return;
378 }
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));
384 };
385
386 /**
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.
390 * @private
391 */
392 NaClManager.prototype.handleStopped_ = function() {
393 this.recognizerState_ = ManagerState_.STOPPED;
394 if (this.restartOnStop_) {
395 this.restartOnStop_ = false;
396 this.startRecognizer();
397 }
398 };
399
400 /**
401 * Handles a message from the NaCl plugin.
402 * @param {!Event} msg Message from NaCl plugin.
403 * @private
404 */
405 NaClManager.prototype.handlePluginMessage_ = function(msg) {
406 if (msg['data']) {
407 this.receivedMessage_(msg['data']);
408 switch (msg['data']) {
409 case hotword.constants.NaClPlugin.REQUEST_MODEL:
410 this.handleRequestModel_();
411 break;
412 case hotword.constants.NaClPlugin.MODEL_LOADED:
413 this.handleModelLoaded_();
414 break;
415 case hotword.constants.NaClPlugin.MS_CONFIGURED:
416 this.handleMsConfigured_();
417 break;
418 case hotword.constants.NaClPlugin.READY_FOR_AUDIO:
419 this.handleReadyForAudio_();
420 break;
421 case hotword.constants.NaClPlugin.HOTWORD_DETECTED:
422 this.handleHotwordDetected_();
423 break;
424 case hotword.constants.NaClPlugin.STOPPED:
425 this.handleStopped_();
426 break;
427 }
428 }
429 };
430
431 return {
432 NaClManager: NaClManager
433 };
434
435 });
OLDNEW
« no previous file with comments | « chrome/browser/resources/hotword/manifest.json ('k') | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698