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.
5 cr.define('hotword', function() {
9 * Class to manage hotwording state. Starts/stops the hotword detector based
10 * on user settings, session requests, and any other factors that play into
11 * whether or not hotwording should be running.
15 function StateManager() {
18 * @private {hotword.StateManager.State_}
20 this.state_ = State_.STOPPED;
23 * Current hotwording status.
24 * @private {?chrome.hotwordPrivate.StatusDetails}
26 this.hotwordStatus_ = null;
29 * NaCl plugin manager.
30 * @private {?hotword.NaClManager}
32 this.pluginManager_ = null;
35 * Source of the current hotword session.
36 * @private {?hotword.constants.SessionSource}
38 this.sessionSource_ = null;
41 * Callback to run when the hotword detector has successfully started.
42 * @private {!function()}
44 this.sessionStartedCb_ = null;
47 * Hotword trigger audio notification... a.k.a The Chime (tm).
50 this.chime_ = document.createElement('audio');
52 // Get the initial status.
53 chrome.hotwordPrivate.getStatus(this.handleStatus_.bind(this));
55 // Setup the chime and insert into the page.
56 this.chime_.src = chrome.extension.getURL(
57 hotword.constants.SHARED_MODULE_ROOT + '/audio/chime.wav');
58 document.body.appendChild(this.chime_);
65 StateManager.State_ = {
71 var State_ = StateManager.State_;
73 StateManager.prototype = {
75 * Request status details update. Intended to be called from the
76 * hotwordPrivate.onEnabledChanged() event.
78 updateStatus: function() {
79 chrome.hotwordPrivate.getStatus(this.handleStatus_.bind(this));
83 * Callback for hotwordPrivate.getStatus() function.
84 * @param {chrome.hotwordPrivate.StatusDetails} status Current hotword
88 handleStatus_: function(status) {
89 this.hotwordStatus_ = status;
90 this.updateStateFromStatus_();
94 * Updates state based on the current status.
97 updateStateFromStatus_: function() {
98 if (!this.hotwordStatus_)
101 if (this.hotwordStatus_.enabled) {
102 // Start the detector if there's a session, and shut it down if there
104 // TODO(amistry): Support stacking sessions. This can happen when the
105 // user opens google.com or the NTP, then opens the launcher. Opening
106 // google.com will create one session, and opening the launcher will
107 // create the second. Closing the launcher should re-activate the
108 // google.com session.
109 // NOTE(amistry): With always-on, we want a different behaviour with
110 // sessions since the detector should always be running. The exception
111 // being when the user triggers by saying 'Ok Google'. In that case, the
112 // detector stops, so starting/stopping the launcher session should
113 // restart the detector.
114 if (this.sessionSource_)
115 this.startDetector_();
117 this.shutdownDetector_();
119 // Not enabled. Shut down if running.
120 this.shutdownDetector_();
125 * Starts the hotword detector.
128 startDetector_: function() {
129 // Last attempt to start detector resulted in an error.
130 if (this.state_ == State_.ERROR) {
131 // TODO(amistry): Do some error rate tracking here and disable the
132 // extension if we error too often.
135 if (!this.pluginManager_) {
136 this.state_ = State_.STARTING;
137 this.pluginManager_ = new hotword.NaClManager();
138 this.pluginManager_.addEventListener(hotword.constants.Event.READY,
139 this.onReady_.bind(this));
140 this.pluginManager_.addEventListener(hotword.constants.Event.ERROR,
141 this.onError_.bind(this));
142 this.pluginManager_.addEventListener(hotword.constants.Event.TRIGGER,
143 this.onTrigger_.bind(this));
144 chrome.runtime.getPlatformInfo(function(platform) {
145 var naclArch = platform.nacl_arch;
147 // googDucking set to false so that audio output level from other tabs
148 // is not affected when hotword is enabled. https://crbug.com/357773
149 // content/common/media/media_stream_options.cc
150 var constraints = /** @type {googMediaStreamConstraints} */
151 ({audio: {optional: [{googDucking: false}]}});
152 navigator.webkitGetUserMedia(
153 /** @type {MediaStreamConstraints} */ (constraints),
155 if (!this.pluginManager_.initialize(naclArch, stream)) {
156 this.state_ = State_.ERROR;
157 this.shutdownPluginManager_();
161 this.state_ = State_.ERROR;
162 this.pluginManager_ = null;
165 } else if (this.state_ != State_.STARTING) {
166 // Don't try to start a starting detector.
167 this.startRecognizer_();
172 * Start the recognizer plugin. Assumes the plugin has been loaded and is
176 startRecognizer_: function() {
177 assert(this.pluginManager_);
178 if (this.state_ != State_.RUNNING) {
179 this.state_ = State_.RUNNING;
180 this.pluginManager_.startRecognizer();
182 if (this.sessionStartedCb_) {
183 this.sessionStartedCb_();
184 this.sessionStartedCb_ = null;
189 * Shuts down and removes the plugin manager, if it exists.
192 shutdownPluginManager_: function() {
193 if (this.pluginManager_) {
194 this.pluginManager_.shutdown();
195 this.pluginManager_ = null;
200 * Shuts down the hotword detector.
203 shutdownDetector_: function() {
204 this.state_ = State_.STOPPED;
205 this.shutdownPluginManager_();
209 * Handle the hotword plugin being ready to start.
212 onReady_: function() {
213 if (this.state_ != State_.STARTING) {
214 // At this point, we should not be in the RUNNING state. Doing so would
215 // imply the hotword detector was started without being ready.
216 assert(this.state_ != State_.RUNNING);
217 this.shutdownPluginManager_();
220 this.startRecognizer_();
224 * Handle an error from the hotword plugin.
227 onError_: function() {
228 this.state_ = State_.ERROR;
229 this.shutdownPluginManager_();
233 * Handle hotword triggering.
236 onTrigger_: function() {
237 assert(this.pluginManager_);
238 // Detector implicitly stops when the hotword is detected.
239 this.state_ = State_.STOPPED;
244 chrome.hotwordPrivate.notifyHotwordRecognition('search', function() {});
246 // Implicitly clear the session. A session needs to be started in order to
247 // restart the detector.
248 this.sessionSource_ = null;
249 this.sessionStartedCb_ = null;
253 * Start a hotwording session.
254 * @param {!hotword.constants.SessionSource} source Source of the hotword
256 * @param {!function()} startedCb Callback invoked when the session has
257 * been started successfully.
259 startSession: function(source, startedCb) {
260 this.sessionSource_ = source;
261 this.sessionStartedCb_ = startedCb;
262 this.updateStateFromStatus_();
266 * Stops a hotwording session.
267 * @param {!hotword.constants.SessionSource} source Source of the hotword
270 stopSession: function(source) {
271 this.sessionSource_ = null;
272 this.sessionStartedCb_ = null;
273 this.updateStateFromStatus_();
278 StateManager: StateManager