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

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: 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
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 * Contains messages for the NaCl recognizer plugin before it is ready to
19 * receive messages.
20 * @private {!Array.<string>}
21 */
22 this.deferredPluginMessages_ = [];
23
24 /**
25 * Current state of this manager.
26 * @private {hotword.NaClManager.ManagerState_}
27 */
28 this.recognizerState_ = ManagerState_.OFF;
29
30 /**
31 * The window.timeout Id associated with a pending message.
James Hawkins 2014/08/13 14:13:18 s/Id/ID/
Anand Mistry (off Chromium) 2014/08/15 04:27:01 Done.
32 * @type {?number}
James Hawkins 2014/08/13 14:13:17 @private {?number} Here and elsewhere.
Anand Mistry (off Chromium) 2014/08/15 04:27:01 Done. But I wonder, what's the different between t
33 * @private
34 */
35 this.naclTimeoutId_ = null;
36
37 /**
38 * The expected message that will cancel the current timeout.
39 * @type {string}
40 * @private
41 */
42 this.expectingMessage_ = null;
43
44 /**
45 * Whether the plugin will be started as soon as it stops.
46 * @type {boolean}
47 * @private
48 */
49 this.restartOnStop_ = false;
50
51 /**
52 * NaCl plugin element on extension background page.
53 * @private {Nacl}
54 */
55 this.plugin_ = null;
56
57 /**
58 * Url containing hotword-model data file.
James Hawkins 2014/08/13 14:13:18 s/Url/URL/
Anand Mistry (off Chromium) 2014/08/15 04:27:00 Done.
59 * @private {string}
60 */
61 this.modelUrl_ = '';
62
63 /**
64 * Media stream containing an audio input track.
65 * @private {MediaStream}
66 */
67 this.stream_ = null;
68 };
69
70
71 /**
72 * States this manager can be in. Since messages between us and the plugin are
73 * asynchronous (and potentially queued), we don't know what state the plugin is
James Hawkins 2014/08/13 14:13:17 Optional nit: Consider removing the ambiguous pron
Anand Mistry (off Chromium) 2014/08/15 04:27:00 Done.
74 * in. However, we can track a state machine for ourself based on what messages
75 * we send/receive.
76 * @enum {number}
77 * @private
78 */
79 NaClManager.ManagerState_ = {
80 OFF: 0,
81 LOADING: 1,
82 STOPPING: 2,
83 STOPPED: 3,
84 STARTING: 4,
85 RUNNING: 5,
86 ERROR: 6,
87 SHUTDOWN: 7,
88 };
89 var ManagerState_ = NaClManager.ManagerState_;
90 var Error_ = hotword.constants.Error;
91
92
James Hawkins 2014/08/13 14:13:17 nit: Remove double blank line.
Anand Mistry (off Chromium) 2014/08/15 04:27:01 Done. Removed everywhere, which seems to be consis
93 NaClManager.prototype.__proto__ = cr.EventTarget.prototype;
94
95
96 /**
97 * Called when an error occurs. Dispatches an event.
98 * @param {hotword.constants.Error} error
99 * @private
100 */
101 NaClManager.prototype.handleError_ = function(error) {
102 event = new Event(hotword.constants.Event.ERROR);
103 event.data = error;
104 this.dispatchEvent(event);
105 };
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 /**
117 * Set a timeout. Only allow one timeout to exist at any given time.
118 * @param {Function} func
James Hawkins 2014/08/13 14:13:17 Prefer the more-specific type 'function(param, lis
Anand Mistry (off Chromium) 2014/08/15 04:27:01 Done.
119 * @param {number} timeout
120 * @private
121 */
122 NaClManager.prototype.setTimeout_ = function(func, timeout) {
123 assert(!this.naclTimeoutId_);
124 this.naclTimeoutId_ = window.setTimeout(
125 function() {
126 this.naclTimeoutId_ = null;
127 func();
128 }.bind(this), timeout);
129 };
130
131
132 /**
133 * Clears the current timeout.
134 * @private
135 */
136 NaClManager.prototype.clearTimeout_ = function() {
137 window.clearTimeout(this.naclTimeoutId_);
138 this.naclTimeoutId_ = null;
139 };
140
141
142 /**
143 * Starts a stopped or stopping hotword recognizer (NaCl plugin).
144 */
145 NaClManager.prototype.startRecognizer = function() {
146 if (this.recognizerState_ == ManagerState_.STOPPED) {
147 assert(this.recognizerState_ == ManagerState_.STOPPED);
rpetterson 2014/08/15 00:41:57 Why is this assert necessary? I don't see how it w
Anand Mistry (off Chromium) 2014/08/15 04:27:01 Oops. Artifact of older iteration.
148 this.recognizerState_ = ManagerState_.STARTING;
149 this.sendDataToPlugin_(hotword.constants.NaClPlugin.RESTART);
150 this.waitForMessage_(hotword.constants.Timeout.LONG,
James Hawkins 2014/08/13 14:13:17 nit: The start of parameter rows must align on the
Anand Mistry (off Chromium) 2014/08/15 04:27:01 Done.
151 hotword.constants.NaClPlugin.READY_FOR_AUDIO);
152 } else if (this.recognizerState_ == ManagerState_.STOPPING) {
153 // Wait until the plugin is stopped before trying to start it.
154 this.restartOnStop_ = true;
155 } else {
156 throw 'Attempting to start NaCl recogniser not in STOPPED or STOPPING ' +
157 'state';
158 }
159 };
160
161
162 /**
163 * Stops the hotword recognizer.
164 */
165 NaClManager.prototype.stopRecognizer = function() {
166 this.sendDataToPlugin_(hotword.constants.NaClPlugin.STOP);
167 this.recognizerState_ = ManagerState_.STOPPING;
168 this.waitForMessage_(hotword.constants.Timeout.NORMAL,
169 hotword.constants.NaClPlugin.STOPPED);
170 };
171
172
173 /**
174 * Checks whether the file at the given path exists.
175 * @param {string} path Path to a file. Can be any valid URL.
176 * @return {boolean} True if the patch exists.
177 * @private
178 */
179 NaClManager.prototype.fileExists_ = function(path) {
180 var xhr = new XMLHttpRequest();
181 xhr.open('HEAD', path, false);
182 try {
183 xhr.send();
184 } catch (err) {
185 return false;
186 }
187 if (xhr.readyState != xhr.DONE || xhr.status != 200) {
188 return false;
189 }
190 return true;
191 };
192
193
194 /**
195 * Initializes the NaCl manager.
196 * @param {string} naclArch Either 'arm', 'x86-32' or 'x86-64'.
197 * @param {MediaStream} stream A stream containing an audio source track.
198 * @return {boolean} True if the successful.
199 */
200 NaClManager.prototype.initialize = function(naclArch, stream) {
James Hawkins 2014/08/13 14:13:17 This method is quite long. I suggest breaking it
Anand Mistry (off Chromium) 2014/08/15 04:27:01 Done.
201 assert(this.recognizerState_ == ManagerState_.OFF);
202 // Create array used to search first for language-country, if not found then
203 // search for language, if not found then no language (empty string).
204 // For example, search for 'en-us', then 'en', then ''.
205 var langs = new Array();
206 if (hotword.constants.UI_LANGUAGE) {
207 // Chrome webstore doesn't support uppercase path: crbug.com/353407
208 var language = hotword.constants.UI_LANGUAGE.toLowerCase();
209 langs.push(language); // Example: 'en-us'.
210 // Remove country to add just the language to array.
211 var hyphen = language.lastIndexOf('-');
212 if (hyphen >= 0) {
213 langs.push(language.substr(0, hyphen)); // Example: 'en'.
214 }
215 }
216 langs.push('');
217 var i, j;
218 // For country-lang variations. For example, when combined with path it will
219 // attempt to find: '/x86-32_en-gb/', else '/x86-32_en/', else '/x86-32_/'.
220 for (i = 0; i < langs.length; i++) {
221 var folder = hotword.constants.SHARED_MODULE_ROOT + '/_platform_specific/' +
222 naclArch + '_' + langs[i] + '/';
223 var dataSrc = folder + hotword.constants.File.RECOGNIZER_CONFIG;
224 var dataExists = this.fileExists_(dataSrc);
225 if (!dataExists) {
226 console.log('File does not exist: ' + dataSrc);
227 continue;
228 }
229 // If the data file exists, assume a valid NaCl nmf also exists.
rpetterson 2014/08/15 00:41:57 how expensive is it to check?
Anand Mistry (off Chromium) 2014/08/15 04:27:00 Cheap. I've added the check.
230 var pluginSrc = hotword.constants.SHARED_MODULE_ROOT + '/hotword_' +
231 langs[i] + '.nmf';
232
233 // Found the correct path. Use it to derive .nmf name and model url.
234 // Example: If path is '_platform_specific/x86-32_en-gb/', then
235 // use lang 'en-gb' to create embed element for 'hotword_en-gb.nmf'.
236 var plugin = document.createElement('embed');
237 plugin.src = pluginSrc;
238 plugin.type = 'application/x-nacl';
239 document.body.appendChild(plugin);
240 this.plugin_ = /** @type {Nacl} */ (plugin);
241 if (!this.plugin_ || !this.plugin_.postMessage) {
242 this.recognizerState_ = ManagerState_.ERROR;
243 return false;
244 }
245 this.modelUrl_ = chrome.extension.getURL(dataSrc);
246 this.stream_ = stream;
247 this.recognizerState_ = ManagerState_.LOADING;
248
249 plugin.addEventListener('message', this.handlePluginMessage_.bind(this),
250 false);
251
252 plugin.addEventListener('crash', function(error) {
253 this.handleError_(Error_.NACL_CRASH);
254 }.bind(this), false);
255 return true;
256 }
257 this.recognizerState_ = ManagerState_.ERROR;
258 return false;
259 };
260
261
262 /**
263 * Shut down the NaCl plugin and free all resources.
James Hawkins 2014/08/13 14:13:17 Shuts
Anand Mistry (off Chromium) 2014/08/15 04:27:01 Done.
264 */
265 NaClManager.prototype.shutdown = function() {
266 if (this.plugin_ != null) {
267 document.body.removeChild(this.plugin_);
268 this.plugin_ = null;
269 }
270 this.clearTimeout_();
271 this.recognizerState_ = ManagerState_.SHUTDOWN;
272 };
273
274
275 /**
276 * Sends data to the NaCl plugin.
277 * @param {string} data Command to be sent to NaCl plugin.
278 * @private
279 */
280 NaClManager.prototype.sendDataToPlugin_ = function(data) {
281 if (this.recognizerState_ != ManagerState_.OFF) {
282 while (this.deferredPluginMessages_.length > 0) {
283 this.plugin_.postMessage(this.deferredPluginMessages_.shift());
284 }
285 this.plugin_.postMessage(data);
286 } else {
287 this.deferredPluginMessages_.push(data);
rpetterson 2014/08/15 00:41:57 Is it not possible to have deferred messages if th
Anand Mistry (off Chromium) 2014/08/15 04:27:00 Hm. We shouldn't be trying to send messages at all
288 }
289 };
290
291
292 /**
293 * Waits, with a timeout, for a message to be received from the plugin. If the
294 * message is not seen within the timeout, dispatch an 'error' event and go into
295 * the ERROR state.
296 * @param {number} timeout Timeout, in milliseconds, to wait for the message.
297 * @param {string} message Message to wait for.
298 * @private
299 */
300 NaClManager.prototype.waitForMessage_ = function(timeout, message) {
301 if (this.expectingMessage_) {
rpetterson 2014/08/15 00:41:57 Is it possible for this function to get called twi
Anand Mistry (off Chromium) 2014/08/15 04:27:01 It used to be in an earlier iteration, but I chang
302 console.log('Existing wait: ' + this.expectingMessage_);
James Hawkins 2014/08/13 14:13:17 Remove console logging. Here and elsewhere.
Anand Mistry (off Chromium) 2014/08/15 04:27:01 Done, but I'd like to add it back (in a future CL)
303 this.clearTimeout_();
304 this.expectingMessage_ = null;
305 }
306 this.setTimeout_(
307 function() {
308 console.log('Timeout waiting for message: ' + message);
309 this.recognizerState_ = ManagerState_.ERROR;
310 this.handleError_(Error_.TIMEOUT);
311 }.bind(this), timeout);
312 this.expectingMessage_ = message;
313 };
314
315
316 /**
317 * Called when a message is received from the plugin. If we're waiting for that
318 * message, cancel the pending timeout.
319 * @param {string} message Message received.
320 * @private
321 */
322 NaClManager.prototype.receivedMessage_ = function(message) {
323 if (message == this.expectingMessage_) {
324 this.clearTimeout_();
325 this.expectingMessage_ = null;
326 }
327 };
328
329
330 /**
331 * Handles a message from the NaCl plugin.
332 * @param {Event} msg Message from NaCl plugin.
333 * @private
334 */
335 NaClManager.prototype.handlePluginMessage_ = function(msg) {
James Hawkins 2014/08/13 14:13:18 I suggest also breaking up this method into sub-ha
Anand Mistry (off Chromium) 2014/08/15 04:27:00 Done.
336 if (msg['data']) {
337 this.receivedMessage_(msg['data']);
338 if (msg['data'] == hotword.constants.NaClPlugin.REQUEST_MODEL) {
339 if (this.recognizerState_ != ManagerState_.LOADING) {
340 console.log('Unexpected state: ' + this.recognizerState_);
341 return;
342 }
343 this.sendDataToPlugin_(
344 hotword.constants.NaClPlugin.MODEL_PREFIX + this.modelUrl_);
345 this.waitForMessage_(hotword.constants.Timeout.LONG,
346 hotword.constants.NaClPlugin.MODEL_LOADED);
347 return;
348 }
349 if (msg['data'] == hotword.constants.NaClPlugin.MODEL_LOADED) {
350 if (this.recognizerState_ != ManagerState_.LOADING) {
351 console.log('Unexpected state: ' + this.recognizerState_);
352 return;
353 }
354 this.sendDataToPlugin_(this.stream_.getAudioTracks()[0]);
355 this.waitForMessage_(hotword.constants.Timeout.LONG,
356 hotword.constants.NaClPlugin.MS_CONFIGURED);
357 return;
358 }
359 if (msg['data'] == hotword.constants.NaClPlugin.MS_CONFIGURED) {
360 if (this.recognizerState_ != ManagerState_.LOADING) {
361 console.log('Unexpected state: ' + this.recognizerState_);
362 return;
363 }
364 this.recognizerState_ = ManagerState_.STOPPED;
365 this.dispatchEvent(new Event(hotword.constants.Event.READY));
366 return;
367 }
368 if (msg['data'] == hotword.constants.NaClPlugin.READY_FOR_AUDIO) {
369 if (this.recognizerState_ != ManagerState_.STARTING) {
370 console.log('Unexpected state: ' + this.recognizerState_);
371 return;
372 }
373 this.recognizerState_ = ManagerState_.RUNNING;
374 return;
375 }
376 if (msg['data'] == hotword.constants.NaClPlugin.HOTWORD_DETECTED) {
377 if (this.recognizerState_ != ManagerState_.RUNNING) {
378 console.log('Unexpected state: ' + this.recognizerState_);
379 return;
380 }
381 // We'll receive a STOPPED message very soon.
382 this.recognizerState_ = ManagerState_.STOPPING;
383 this.waitForMessage_(hotword.constants.Timeout.NORMAL,
384 hotword.constants.NaClPlugin.STOPPED);
385 this.dispatchEvent(new Event(hotword.constants.Event.TRIGGER));
386 return;
387 }
388 if (msg['data'] == hotword.constants.NaClPlugin.STOPPED) {
389 this.recognizerState_ = ManagerState_.STOPPED;
390 if (this.restartOnStop_) {
391 this.restartOnStop_ = false;
392 this.startRecognizer();
393 }
394 return;
395 }
396 }
397 };
398
399 return {
400 NaClManager: NaClManager
401 };
402
403 });
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698