Home | History | Annotate | Download | only in webapp
      1 // Copyright 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 /**
      6  * @fileoverview Description of this file.
      7  * Class handling interaction with the cast extension session of the Chromoting
      8  * host. It receives and sends extension messages from/to the host through
      9  * the client session. It uses the Google Cast Chrome Sender API library to
     10  * interact with nearby Cast receivers.
     11  *
     12  * Once it establishes connection with a Cast device (upon user choice), it
     13  * creates a session, loads our registered receiver application and then becomes
     14  * a message proxy between the host and cast device, helping negotiate
     15  * their peer connection.
     16  */
     17 
     18 'use strict';
     19 
     20 /** @suppress {duplicate} */
     21 var remoting = remoting || {};
     22 
     23 /**
     24  * @constructor
     25  * @param {!remoting.ClientSession} clientSession The client session to send
     26  * cast extension messages to.
     27  */
     28 remoting.CastExtensionHandler = function(clientSession) {
     29   /** @private */
     30   this.clientSession_ = clientSession;
     31 
     32   /** @type {chrome.cast.Session} @private */
     33   this.session_ = null;
     34 
     35   /** @type {string} @private */
     36   this.kCastNamespace_ = 'urn:x-cast:com.chromoting.cast.all';
     37 
     38   /** @type {string} @private */
     39   this.kApplicationId_ = "8A1211E3";
     40 
     41   /** @type {Array.<Object>} @private */
     42   this.messageQueue_ = [];
     43 
     44   this.start_();
     45 };
     46 
     47 /**
     48  * The id of the script node.
     49  * @type {string}
     50  * @private
     51  */
     52 remoting.CastExtensionHandler.prototype.SCRIPT_NODE_ID_ = 'cast-script-node';
     53 
     54 /**
     55  * Attempts to load the Google Cast Chrome Sender API libary.
     56  * @private
     57  */
     58 remoting.CastExtensionHandler.prototype.start_ = function() {
     59   var node = document.getElementById(this.SCRIPT_NODE_ID_);
     60   if (node) {
     61     console.error(
     62         'Multiple calls to CastExtensionHandler.start_ not expected.');
     63     return;
     64   }
     65 
     66   // Create a script node to load the Cast Sender API.
     67   node = document.createElement('script');
     68   node.id = this.SCRIPT_NODE_ID_;
     69   node.src = "https://www.gstatic.com/cv/js/sender/v1/cast_sender.js";
     70   node.type = 'text/javascript';
     71   document.body.insertBefore(node, document.body.firstChild);
     72 
     73   /** @type {remoting.CastExtensionHandler} */
     74   var that = this;
     75   var onLoad = function() {
     76     window['__onGCastApiAvailable'] = that.onGCastApiAvailable.bind(that);
     77 
     78   };
     79   var onLoadError = function(event) {
     80     console.error("Failed to load Chrome Cast Sender API.");
     81   }
     82   node.addEventListener('load', onLoad, false);
     83   node.addEventListener('error', onLoadError, false);
     84 
     85 };
     86 
     87 /**
     88  * Process Cast Extension Messages from the Chromoting host.
     89  * @param {string} msgString The extension message's data.
     90  */
     91 remoting.CastExtensionHandler.prototype.onMessage = function(msgString) {
     92   var message = getJsonObjectFromString(msgString);
     93 
     94   // Save messages to send after a session is established.
     95   this.messageQueue_.push(message);
     96   // Trigger the sending of pending messages, followed by the one just
     97   // received.
     98   if (this.session_) {
     99     this.sendPendingMessages_();
    100   }
    101 };
    102 
    103 /**
    104  * Send cast-extension messages through the client session.
    105  * @param {Object} response The JSON response to be sent to the host. The
    106  * response object must contain the appropriate keys.
    107  * @private
    108  */
    109 remoting.CastExtensionHandler.prototype.sendMessageToHost_ =
    110     function(response) {
    111   this.clientSession_.sendCastExtensionMessage(response);
    112 };
    113 
    114 /**
    115  * Send pending messages from the host to the receiver app.
    116  * @private
    117  */
    118 remoting.CastExtensionHandler.prototype.sendPendingMessages_ = function() {
    119   var len = this.messageQueue_.length;
    120   for(var i = 0; i<len; i++) {
    121     this.session_.sendMessage(this.kCastNamespace_,
    122                               this.messageQueue_[i],
    123                               this.sendMessageSuccess.bind(this),
    124                               this.sendMessageFailure.bind(this));
    125   }
    126   this.messageQueue_ = [];
    127 };
    128 
    129 /**
    130  * Event handler for '__onGCastApiAvailable' window event. This event is
    131  * triggered if the Google Cast Chrome Sender API is available. We attempt to
    132  * load this API in this.start(). If the API loaded successfully, we can proceed
    133  * to initialize it and configure it to launch our Cast Receiver Application.
    134  *
    135  * @param {boolean} loaded True if the API loaded succesfully.
    136  * @param {Object} errorInfo Info if the API load failed.
    137  */
    138 remoting.CastExtensionHandler.prototype.onGCastApiAvailable =
    139     function(loaded, errorInfo) {
    140   if (loaded) {
    141     this.initializeCastApi();
    142   } else {
    143     console.log(errorInfo);
    144   }
    145 };
    146 
    147 /**
    148  * Initialize the Cast API.
    149  * @private
    150  */
    151 remoting.CastExtensionHandler.prototype.initializeCastApi = function() {
    152   var sessionRequest = new chrome.cast.SessionRequest(this.kApplicationId_);
    153   var apiConfig =
    154       new chrome.cast.ApiConfig(sessionRequest,
    155                                 this.sessionListener.bind(this),
    156                                 this.receiverListener.bind(this),
    157                                 chrome.cast.AutoJoinPolicy.PAGE_SCOPED,
    158                                 chrome.cast.DefaultActionPolicy.CREATE_SESSION);
    159   chrome.cast.initialize(
    160       apiConfig, this.onInitSuccess.bind(this), this.onInitError.bind(this));
    161 };
    162 
    163 /**
    164  * Callback for successful initialization of the Cast API.
    165  */
    166 remoting.CastExtensionHandler.prototype.onInitSuccess = function() {
    167   console.log("Initialization Successful.");
    168 };
    169 
    170 /**
    171  * Callback for failed initialization of the Cast API.
    172  */
    173 remoting.CastExtensionHandler.prototype.onInitError = function() {
    174   console.error("Initialization Failed.");
    175 };
    176 
    177 /**
    178  * Listener invoked when a session is created or connected by the SDK.
    179  * Note: The requestSession method would not cause this callback to be invoked
    180  * since it is passed its own listener.
    181  * @param {chrome.cast.Session} session The resulting session.
    182  */
    183 remoting.CastExtensionHandler.prototype.sessionListener = function(session) {
    184   console.log('New Session:' + /** @type {string} */ (session.sessionId));
    185   this.session_ = session;
    186   if (this.session_.media.length != 0) {
    187 
    188     // There should be no media associated with the session, since we never
    189     // directly load media from the Sender application.
    190     this.onMediaDiscovered('sessionListener', this.session_.media[0]);
    191   }
    192   this.session_.addMediaListener(
    193       this.onMediaDiscovered.bind(this, 'addMediaListener'));
    194   this.session_.addUpdateListener(this.sessionUpdateListener.bind(this));
    195   this.session_.addMessageListener(this.kCastNamespace_,
    196                                    this.chromotingMessageListener.bind(this));
    197   this.session_.sendMessage(this.kCastNamespace_,
    198       {subject : 'test', chromoting_data : 'Hello, Cast.'},
    199       this.sendMessageSuccess.bind(this),
    200       this.sendMessageFailure.bind(this));
    201   this.sendPendingMessages_();
    202 };
    203 
    204 /**
    205  * Listener invoked when a media session is created by another sender.
    206  * @param {string} how How this callback was triggered.
    207  * @param {chrome.cast.media.Media} media The media item discovered.
    208  * @private
    209  */
    210 remoting.CastExtensionHandler.prototype.onMediaDiscovered =
    211     function(how, media) {
    212       console.error("Unexpected media session discovered.");
    213 };
    214 
    215 /**
    216  * Listener invoked when a cast extension message was sent to the cast device
    217  * successfully.
    218  * @private
    219  */
    220 remoting.CastExtensionHandler.prototype.sendMessageSuccess = function() {
    221 };
    222 
    223 /**
    224  * Listener invoked when a cast extension message failed to be sent to the cast
    225  * device.
    226  * @param {Object} error The error.
    227  * @private
    228  */
    229 remoting.CastExtensionHandler.prototype.sendMessageFailure = function(error) {
    230   console.error('Failed to Send Message.', error);
    231 };
    232 
    233 /**
    234  * Listener invoked when a cast extension message is received from the Cast
    235  * device.
    236  * @param {string} ns The namespace of the message received.
    237  * @param {string} message The stringified JSON message received.
    238  */
    239 remoting.CastExtensionHandler.prototype.chromotingMessageListener =
    240     function(ns, message) {
    241   if (ns === this.kCastNamespace_) {
    242     try {
    243         var messageObj = getJsonObjectFromString(message);
    244         this.sendMessageToHost_(messageObj);
    245     } catch (err) {
    246       console.error('Failed to process message from Cast device.');
    247     }
    248   } else {
    249     console.error("Unexpected message from Cast device.");
    250   }
    251 };
    252 
    253 /**
    254  * Listener invoked when there updates to the current session.
    255  *
    256  * @param {boolean} isAlive True if the session is still alive.
    257  */
    258 remoting.CastExtensionHandler.prototype.sessionUpdateListener =
    259     function(isAlive) {
    260   var message = isAlive ? 'Session Updated' : 'Session Removed';
    261   message += ': ' + this.session_.sessionId +'.';
    262   console.log(message);
    263 };
    264 
    265 /**
    266  * Listener invoked when the availability of a Cast receiver that supports
    267  * the application in sessionRequest is known or changes.
    268  *
    269  * @param {chrome.cast.ReceiverAvailability} availability Receiver availability.
    270  */
    271 remoting.CastExtensionHandler.prototype.receiverListener =
    272     function(availability) {
    273   if (availability === chrome.cast.ReceiverAvailability.AVAILABLE) {
    274     console.log("Receiver(s) Found.");
    275   } else {
    276     console.error("No Receivers Available.");
    277   }
    278 };
    279 
    280 /**
    281  * Launches the associated receiver application by requesting that it be created
    282  * on the Cast device. It uses the SessionRequest passed during initialization
    283  * to determine what application to launch on the Cast device.
    284  *
    285  * Note: This method is intended to be used as a click listener for a custom
    286  * cast button on the webpage. We currently use the default cast button in
    287  * Chrome, so this method is unused.
    288  */
    289 remoting.CastExtensionHandler.prototype.launchApp = function() {
    290   chrome.cast.requestSession(this.onRequestSessionSuccess.bind(this),
    291                              this.onLaunchError.bind(this));
    292 };
    293 
    294 /**
    295  * Listener invoked when chrome.cast.requestSession completes successfully.
    296  *
    297  * @param {chrome.cast.Session} session The requested session.
    298  */
    299 remoting.CastExtensionHandler.prototype.onRequestSessionSuccess =
    300     function (session) {
    301   this.session_ = session;
    302   this.session_.addUpdateListener(this.sessionUpdateListener.bind(this));
    303   if (this.session_.media.length != 0) {
    304     this.onMediaDiscovered('onRequestSession', this.session_.media[0]);
    305   }
    306   this.session_.addMediaListener(
    307       this.onMediaDiscovered.bind(this, 'addMediaListener'));
    308   this.session_.addMessageListener(this.kCastNamespace_,
    309                                    this.chromotingMessageListener.bind(this));
    310 };
    311 
    312 /**
    313  * Listener invoked when chrome.cast.requestSession fails.
    314  * @param {chrome.cast.Error} error The error code.
    315  */
    316 remoting.CastExtensionHandler.prototype.onLaunchError = function(error) {
    317   console.error("Error Casting to Receiver.", error);
    318 };
    319 
    320 /**
    321  * Stops the running receiver application associated with the session.
    322  * TODO(aiguha): When the user disconnects using the blue drop down bar,
    323  * the client session should notify the CastExtensionHandler, which should
    324  * call this method to close the session with the Cast device.
    325  */
    326 remoting.CastExtensionHandler.prototype.stopApp = function() {
    327   this.session_.stop(this.onStopAppSuccess.bind(this),
    328                      this.onStopAppError.bind(this));
    329 };
    330 
    331 /**
    332  * Listener invoked when the receiver application is stopped successfully.
    333  */
    334 remoting.CastExtensionHandler.prototype.onStopAppSuccess = function() {
    335 };
    336 
    337 /**
    338  * Listener invoked when we fail to stop the receiver application.
    339  *
    340  * @param {chrome.cast.Error} error The error code.
    341  */
    342 remoting.CastExtensionHandler.prototype.onStopAppError = function(error) {
    343   console.error('Error Stopping App: ', error);
    344 };
    345