Home | History | Annotate | Download | only in hotword
      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 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.
     12    * @constructor
     13    * @struct
     14    */
     15   function StateManager() {
     16     /**
     17      * Current state.
     18      * @private {hotword.StateManager.State_}
     19      */
     20     this.state_ = State_.STOPPED;
     21 
     22     /**
     23      * Current hotwording status.
     24      * @private {?chrome.hotwordPrivate.StatusDetails}
     25      */
     26     this.hotwordStatus_ = null;
     27 
     28     /**
     29      * NaCl plugin manager.
     30      * @private {?hotword.NaClManager}
     31      */
     32     this.pluginManager_ = null;
     33 
     34     /**
     35      * Source of the current hotword session.
     36      * @private {?hotword.constants.SessionSource}
     37      */
     38     this.sessionSource_ = null;
     39 
     40     /**
     41      * Callback to run when the hotword detector has successfully started.
     42      * @private {!function()}
     43      */
     44     this.sessionStartedCb_ = null;
     45 
     46     /**
     47      * Hotword trigger audio notification... a.k.a The Chime (tm).
     48      * @private {!Audio}
     49      */
     50     this.chime_ = document.createElement('audio');
     51 
     52     // Get the initial status.
     53     chrome.hotwordPrivate.getStatus(this.handleStatus_.bind(this));
     54 
     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_);
     59   }
     60 
     61   /**
     62    * @enum {number}
     63    * @private
     64    */
     65   StateManager.State_ = {
     66     STOPPED: 0,
     67     STARTING: 1,
     68     RUNNING: 2,
     69     ERROR: 3,
     70   };
     71   var State_ = StateManager.State_;
     72 
     73   StateManager.prototype = {
     74     /**
     75      * Request status details update. Intended to be called from the
     76      * hotwordPrivate.onEnabledChanged() event.
     77      */
     78     updateStatus: function() {
     79       chrome.hotwordPrivate.getStatus(this.handleStatus_.bind(this));
     80     },
     81 
     82     /**
     83      * Callback for hotwordPrivate.getStatus() function.
     84      * @param {chrome.hotwordPrivate.StatusDetails} status Current hotword
     85      *     status.
     86      * @private
     87      */
     88     handleStatus_: function(status) {
     89       this.hotwordStatus_ = status;
     90       this.updateStateFromStatus_();
     91     },
     92 
     93     /**
     94      * Updates state based on the current status.
     95      * @private
     96      */
     97     updateStateFromStatus_: function() {
     98       if (!this.hotwordStatus_)
     99         return;
    100 
    101       if (this.hotwordStatus_.enabled) {
    102         // Start the detector if there's a session, and shut it down if there
    103         // isn't.
    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_();
    116         else
    117           this.shutdownDetector_();
    118       } else {
    119         // Not enabled. Shut down if running.
    120         this.shutdownDetector_();
    121       }
    122     },
    123 
    124     /**
    125      * Starts the hotword detector.
    126      * @private
    127      */
    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.
    133       }
    134 
    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;
    146 
    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),
    154               function(stream) {
    155                 if (!this.pluginManager_.initialize(naclArch, stream)) {
    156                   this.state_ = State_.ERROR;
    157                   this.shutdownPluginManager_();
    158                 }
    159               }.bind(this),
    160               function(error) {
    161                 this.state_ = State_.ERROR;
    162                 this.pluginManager_ = null;
    163               }.bind(this));
    164         }.bind(this));
    165       } else if (this.state_ != State_.STARTING) {
    166         // Don't try to start a starting detector.
    167         this.startRecognizer_();
    168       }
    169     },
    170 
    171     /**
    172      * Start the recognizer plugin. Assumes the plugin has been loaded and is
    173      * ready to start.
    174      * @private
    175      */
    176     startRecognizer_: function() {
    177       assert(this.pluginManager_);
    178       if (this.state_ != State_.RUNNING) {
    179         this.state_ = State_.RUNNING;
    180         this.pluginManager_.startRecognizer();
    181       }
    182       if (this.sessionStartedCb_) {
    183         this.sessionStartedCb_();
    184         this.sessionStartedCb_ = null;
    185       }
    186     },
    187 
    188     /**
    189      * Shuts down and removes the plugin manager, if it exists.
    190      * @private
    191      */
    192     shutdownPluginManager_: function() {
    193       if (this.pluginManager_) {
    194         this.pluginManager_.shutdown();
    195         this.pluginManager_ = null;
    196       }
    197     },
    198 
    199     /**
    200      * Shuts down the hotword detector.
    201      * @private
    202      */
    203     shutdownDetector_: function() {
    204       this.state_ = State_.STOPPED;
    205       this.shutdownPluginManager_();
    206     },
    207 
    208     /**
    209      * Handle the hotword plugin being ready to start.
    210      * @private
    211      */
    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_();
    218         return;
    219       }
    220       this.startRecognizer_();
    221     },
    222 
    223     /**
    224      * Handle an error from the hotword plugin.
    225      * @private
    226      */
    227     onError_: function() {
    228       this.state_ = State_.ERROR;
    229       this.shutdownPluginManager_();
    230     },
    231 
    232     /**
    233      * Handle hotword triggering.
    234      * @private
    235      */
    236     onTrigger_: function() {
    237       assert(this.pluginManager_);
    238       // Detector implicitly stops when the hotword is detected.
    239       this.state_ = State_.STOPPED;
    240 
    241       // Play the chime.
    242       this.chime_.play();
    243 
    244       chrome.hotwordPrivate.notifyHotwordRecognition('search', function() {});
    245 
    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;
    250     },
    251 
    252     /**
    253      * Start a hotwording session.
    254      * @param {!hotword.constants.SessionSource} source Source of the hotword
    255      *     session request.
    256      * @param {!function()} startedCb Callback invoked when the session has
    257      *     been started successfully.
    258      */
    259     startSession: function(source, startedCb) {
    260       this.sessionSource_ = source;
    261       this.sessionStartedCb_ = startedCb;
    262       this.updateStateFromStatus_();
    263     },
    264 
    265     /**
    266      * Stops a hotwording session.
    267      * @param {!hotword.constants.SessionSource} source Source of the hotword
    268      *     session request.
    269      */
    270     stopSession: function(source) {
    271       this.sessionSource_ = null;
    272       this.sessionStartedCb_ = null;
    273       this.updateStateFromStatus_();
    274     }
    275   };
    276 
    277   return {
    278     StateManager: StateManager
    279   };
    280 });
    281