Chromium Code Reviews| 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 |