OLD | NEW |
---|---|
(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 }); | |
OLD | NEW |