| OLD | NEW | 
 | (Empty) | 
|    1 // Copyright (c) 2012 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 |  | 
|    7  * Class handling creation and teardown of a remoting client session. |  | 
|    8  * |  | 
|    9  * This abstracts a <embed> element and controls the plugin which does the |  | 
|   10  * actual remoting work.  There should be no UI code inside this class.  It |  | 
|   11  * should be purely thought of as a controller of sorts. |  | 
|   12  */ |  | 
|   13  |  | 
|   14 'use strict'; |  | 
|   15  |  | 
|   16 /** @suppress {duplicate} */ |  | 
|   17 var remoting = remoting || {}; |  | 
|   18  |  | 
|   19 /** |  | 
|   20  * @param {string} hostJid The jid of the host to connect to. |  | 
|   21  * @param {string} hostPublicKey The base64 encoded version of the host's |  | 
|   22  *     public key. |  | 
|   23  * @param {string} authenticationCode The access code for IT2Me or the |  | 
|   24  *     PIN for Me2Me. |  | 
|   25  * @param {string} email The username for the talk network. |  | 
|   26  * @param {function(remoting.ClientSession.State, |  | 
|   27                     remoting.ClientSession.State):void} onStateChange |  | 
|   28  *     The callback to invoke when the session changes state. |  | 
|   29  * @constructor |  | 
|   30  */ |  | 
|   31 remoting.ClientSession = function(hostJid, hostPublicKey, authenticationCode, |  | 
|   32                                   email, onStateChange) { |  | 
|   33   this.state = remoting.ClientSession.State.CREATED; |  | 
|   34  |  | 
|   35   this.hostJid = hostJid; |  | 
|   36   this.hostPublicKey = hostPublicKey; |  | 
|   37   this.authenticationCode = authenticationCode; |  | 
|   38   this.email = email; |  | 
|   39   this.clientJid = ''; |  | 
|   40   this.sessionId = ''; |  | 
|   41   /** @type {remoting.ViewerPlugin} */ |  | 
|   42   this.plugin = null; |  | 
|   43   this.logToServer = new remoting.LogToServer(); |  | 
|   44   this.onStateChange = onStateChange; |  | 
|   45   /** @type {remoting.ClientSession} */ |  | 
|   46   var that = this; |  | 
|   47   /** @type {function():void} @private */ |  | 
|   48   this.refocusPlugin_ = function() { that.plugin.focus(); }; |  | 
|   49 }; |  | 
|   50  |  | 
|   51 // Note that the positive values in both of these enums are copied directly |  | 
|   52 // from chromoting_scriptable_object.h and must be kept in sync. The negative |  | 
|   53 // values represent states transitions that occur within the web-app that have |  | 
|   54 // no corresponding plugin state transition. |  | 
|   55 /** @enum {number} */ |  | 
|   56 remoting.ClientSession.State = { |  | 
|   57   CREATED: -3, |  | 
|   58   BAD_PLUGIN_VERSION: -2, |  | 
|   59   UNKNOWN_PLUGIN_ERROR: -1, |  | 
|   60   UNKNOWN: 0, |  | 
|   61   CONNECTING: 1, |  | 
|   62   INITIALIZING: 2, |  | 
|   63   CONNECTED: 3, |  | 
|   64   CLOSED: 4, |  | 
|   65   CONNECTION_FAILED: 5 |  | 
|   66 }; |  | 
|   67  |  | 
|   68 /** @enum {number} */ |  | 
|   69 remoting.ClientSession.ConnectionError = { |  | 
|   70   NONE: 0, |  | 
|   71   HOST_IS_OFFLINE: 1, |  | 
|   72   SESSION_REJECTED: 2, |  | 
|   73   INCOMPATIBLE_PROTOCOL: 3, |  | 
|   74   NETWORK_FAILURE: 4 |  | 
|   75 }; |  | 
|   76  |  | 
|   77 // Keys for connection statistics. |  | 
|   78 remoting.ClientSession.STATS_KEY_VIDEO_BANDWIDTH = 'video_bandwidth'; |  | 
|   79 remoting.ClientSession.STATS_KEY_VIDEO_FRAME_RATE = 'video_frame_rate'; |  | 
|   80 remoting.ClientSession.STATS_KEY_CAPTURE_LATENCY = 'capture_latency'; |  | 
|   81 remoting.ClientSession.STATS_KEY_ENCODE_LATENCY = 'encode_latency'; |  | 
|   82 remoting.ClientSession.STATS_KEY_DECODE_LATENCY = 'decode_latency'; |  | 
|   83 remoting.ClientSession.STATS_KEY_RENDER_LATENCY = 'render_latency'; |  | 
|   84 remoting.ClientSession.STATS_KEY_ROUNDTRIP_LATENCY = 'roundtrip_latency'; |  | 
|   85  |  | 
|   86 /** |  | 
|   87  * The current state of the session. |  | 
|   88  * @type {remoting.ClientSession.State} |  | 
|   89  */ |  | 
|   90 remoting.ClientSession.prototype.state = remoting.ClientSession.State.UNKNOWN; |  | 
|   91  |  | 
|   92 /** |  | 
|   93  * The last connection error. Set when state is set to CONNECTION_FAILED. |  | 
|   94  * @type {remoting.ClientSession.ConnectionError} |  | 
|   95  */ |  | 
|   96 remoting.ClientSession.prototype.error = |  | 
|   97     remoting.ClientSession.ConnectionError.NONE; |  | 
|   98  |  | 
|   99 /** |  | 
|  100  * Chromoting session API version (for this javascript). |  | 
|  101  * This is compared with the plugin API version to verify that they are |  | 
|  102  * compatible. |  | 
|  103  * |  | 
|  104  * @const |  | 
|  105  * @private |  | 
|  106  */ |  | 
|  107 remoting.ClientSession.prototype.API_VERSION_ = 2; |  | 
|  108  |  | 
|  109 /** |  | 
|  110  * The oldest API version that we support. |  | 
|  111  * This will differ from the |API_VERSION_| if we maintain backward |  | 
|  112  * compatibility with older API versions. |  | 
|  113  * |  | 
|  114  * @const |  | 
|  115  * @private |  | 
|  116  */ |  | 
|  117 remoting.ClientSession.prototype.API_MIN_VERSION_ = 1; |  | 
|  118  |  | 
|  119 /** |  | 
|  120  * Server used to bridge into the Jabber network for establishing Jingle |  | 
|  121  * connections. |  | 
|  122  * |  | 
|  123  * @const |  | 
|  124  * @private |  | 
|  125  */ |  | 
|  126 remoting.ClientSession.prototype.HTTP_XMPP_PROXY_ = |  | 
|  127     'https://chromoting-httpxmpp-oauth2-dev.corp.google.com'; |  | 
|  128  |  | 
|  129 /** |  | 
|  130  * The id of the client plugin |  | 
|  131  * |  | 
|  132  * @const |  | 
|  133  */ |  | 
|  134 remoting.ClientSession.prototype.PLUGIN_ID = 'session-client-plugin'; |  | 
|  135  |  | 
|  136 /** |  | 
|  137  * Callback to invoke when the state is changed. |  | 
|  138  * |  | 
|  139  * @param {remoting.ClientSession.State} oldState The previous state. |  | 
|  140  * @param {remoting.ClientSession.State} newState The current state. |  | 
|  141  */ |  | 
|  142 remoting.ClientSession.prototype.onStateChange = |  | 
|  143     function(oldState, newState) { }; |  | 
|  144  |  | 
|  145 /** |  | 
|  146  * Adds <embed> element to |container| and readies the sesion object. |  | 
|  147  * |  | 
|  148  * @param {Element} container The element to add the plugin to. |  | 
|  149  * @param {string} oauth2AccessToken A valid OAuth2 access token. |  | 
|  150  * @return {void} Nothing. |  | 
|  151  */ |  | 
|  152 remoting.ClientSession.prototype.createPluginAndConnect = |  | 
|  153     function(container, oauth2AccessToken) { |  | 
|  154   this.plugin = /** @type {remoting.ViewerPlugin} */ |  | 
|  155       document.createElement('embed'); |  | 
|  156   this.plugin.id = this.PLUGIN_ID; |  | 
|  157   this.plugin.src = 'about://none'; |  | 
|  158   this.plugin.type = 'pepper-application/x-chromoting'; |  | 
|  159   this.plugin.width = 0; |  | 
|  160   this.plugin.height = 0; |  | 
|  161   this.plugin.tabIndex = 0;  // Required, otherwise focus() doesn't work. |  | 
|  162   container.appendChild(this.plugin); |  | 
|  163  |  | 
|  164   this.plugin.focus(); |  | 
|  165   this.plugin.addEventListener('blur', this.refocusPlugin_, false); |  | 
|  166  |  | 
|  167   if (!this.isPluginVersionSupported_(this.plugin)) { |  | 
|  168     // TODO(ajwong): Remove from parent. |  | 
|  169     delete this.plugin; |  | 
|  170     this.setState_(remoting.ClientSession.State.BAD_PLUGIN_VERSION); |  | 
|  171     return; |  | 
|  172   } |  | 
|  173  |  | 
|  174   /** @type {remoting.ClientSession} */ var that = this; |  | 
|  175   /** @param {string} msg The IQ stanza to send. */ |  | 
|  176   this.plugin.sendIq = function(msg) { that.sendIq_(msg); }; |  | 
|  177   /** @param {string} msg The message to log. */ |  | 
|  178   this.plugin.debugInfo = function(msg) { |  | 
|  179     remoting.debug.log('plugin: ' + msg); |  | 
|  180   }; |  | 
|  181  |  | 
|  182   // TODO(ajwong): Is it even worth having this class handle these events? |  | 
|  183   // Or would it be better to just allow users to pass in their own handlers |  | 
|  184   // and leave these blank by default? |  | 
|  185   /** |  | 
|  186    * @param {number} status The plugin status. |  | 
|  187    * @param {number} error The plugin error status, if any. |  | 
|  188    */ |  | 
|  189   this.plugin.connectionInfoUpdate = function(status, error) { |  | 
|  190     that.connectionInfoUpdateCallback(status, error); |  | 
|  191   }; |  | 
|  192   this.plugin.desktopSizeUpdate = function() { that.onDesktopSizeChanged_(); }; |  | 
|  193  |  | 
|  194   // TODO(garykac): Clean exit if |connect| isn't a function. |  | 
|  195   if (typeof this.plugin.connect === 'function') { |  | 
|  196     this.connectPluginToWcs_(oauth2AccessToken); |  | 
|  197   } else { |  | 
|  198     remoting.debug.log('ERROR: remoting plugin not loaded'); |  | 
|  199     this.setState_(remoting.ClientSession.State.UNKNOWN_PLUGIN_ERROR); |  | 
|  200   } |  | 
|  201 }; |  | 
|  202  |  | 
|  203 /** |  | 
|  204  * Deletes the <embed> element from the container, without sending a |  | 
|  205  * session_terminate request.  This is to be called when the session was |  | 
|  206  * disconnected by the Host. |  | 
|  207  * |  | 
|  208  * @return {void} Nothing. |  | 
|  209  */ |  | 
|  210 remoting.ClientSession.prototype.removePlugin = function() { |  | 
|  211   if (this.plugin) { |  | 
|  212     this.plugin.removeEventListener('blur', this.refocusPlugin_, false); |  | 
|  213     var parentNode = this.plugin.parentNode; |  | 
|  214     parentNode.removeChild(this.plugin); |  | 
|  215     this.plugin = null; |  | 
|  216   } |  | 
|  217 }; |  | 
|  218  |  | 
|  219 /** |  | 
|  220  * Deletes the <embed> element from the container and disconnects. |  | 
|  221  * |  | 
|  222  * @return {void} Nothing. |  | 
|  223  */ |  | 
|  224 remoting.ClientSession.prototype.disconnect = function() { |  | 
|  225   // The plugin won't send a state change notification, so we explicitly log |  | 
|  226   // the fact that the connection has closed. |  | 
|  227   this.logToServer.logClientSessionStateChange( |  | 
|  228       remoting.ClientSession.State.CLOSED, |  | 
|  229       remoting.ClientSession.ConnectionError.NONE); |  | 
|  230   if (remoting.wcs) { |  | 
|  231     remoting.wcs.setOnIq(function(stanza) {}); |  | 
|  232     this.sendIq_( |  | 
|  233         '<cli:iq ' + |  | 
|  234             'to="' + this.hostJid + '" ' + |  | 
|  235             'type="set" ' + |  | 
|  236             'id="session-terminate" ' + |  | 
|  237             'xmlns:cli="jabber:client">' + |  | 
|  238           '<jingle ' + |  | 
|  239               'xmlns="urn:xmpp:jingle:1" ' + |  | 
|  240               'action="session-terminate" ' + |  | 
|  241               'initiator="' + this.clientJid + '" ' + |  | 
|  242               'sid="' + this.sessionId + '">' + |  | 
|  243             '<reason><success/></reason>' + |  | 
|  244           '</jingle>' + |  | 
|  245         '</cli:iq>'); |  | 
|  246   } |  | 
|  247   this.removePlugin(); |  | 
|  248 }; |  | 
|  249  |  | 
|  250 /** |  | 
|  251  * Sends an IQ stanza via the http xmpp proxy. |  | 
|  252  * |  | 
|  253  * @private |  | 
|  254  * @param {string} msg XML string of IQ stanza to send to server. |  | 
|  255  * @return {void} Nothing. |  | 
|  256  */ |  | 
|  257 remoting.ClientSession.prototype.sendIq_ = function(msg) { |  | 
|  258   remoting.debug.logIq(true, msg); |  | 
|  259   // Extract the session id, so we can close the session later. |  | 
|  260   var parser = new DOMParser(); |  | 
|  261   var iqNode = parser.parseFromString(msg, 'text/xml').firstChild; |  | 
|  262   var jingleNode = iqNode.firstChild; |  | 
|  263   if (jingleNode) { |  | 
|  264     var action = jingleNode.getAttribute('action'); |  | 
|  265     if (jingleNode.nodeName == 'jingle' && action == 'session-initiate') { |  | 
|  266       this.sessionId = jingleNode.getAttribute('sid'); |  | 
|  267     } |  | 
|  268   } |  | 
|  269  |  | 
|  270   // Send the stanza. |  | 
|  271   if (remoting.wcs) { |  | 
|  272     remoting.wcs.sendIq(msg); |  | 
|  273   } else { |  | 
|  274     remoting.debug.log('Tried to send IQ before WCS was ready.'); |  | 
|  275     this.setState_(remoting.ClientSession.State.CONNECTION_FAILED); |  | 
|  276   } |  | 
|  277 }; |  | 
|  278  |  | 
|  279 /** |  | 
|  280  * @private |  | 
|  281  * @param {remoting.ViewerPlugin} plugin The embed element for the plugin. |  | 
|  282  * @return {boolean} True if the plugin and web-app versions are compatible. |  | 
|  283  */ |  | 
|  284 remoting.ClientSession.prototype.isPluginVersionSupported_ = function(plugin) { |  | 
|  285   return this.API_VERSION_ >= plugin.apiMinVersion && |  | 
|  286       plugin.apiVersion >= this.API_MIN_VERSION_; |  | 
|  287 }; |  | 
|  288  |  | 
|  289 /** |  | 
|  290  * Connects the plugin to WCS. |  | 
|  291  * |  | 
|  292  * @private |  | 
|  293  * @param {string} oauth2AccessToken A valid OAuth2 access token. |  | 
|  294  * @return {void} Nothing. |  | 
|  295  */ |  | 
|  296 remoting.ClientSession.prototype.connectPluginToWcs_ = |  | 
|  297     function(oauth2AccessToken) { |  | 
|  298   this.clientJid = remoting.wcs.getJid(); |  | 
|  299   if (this.clientJid == '') { |  | 
|  300     remoting.debug.log('Tried to connect without a full JID.'); |  | 
|  301   } |  | 
|  302   remoting.debug.setJids(this.clientJid, this.hostJid); |  | 
|  303   /** @type {remoting.ClientSession} */ |  | 
|  304   var that = this; |  | 
|  305   /** @param {string} stanza The IQ stanza received. */ |  | 
|  306   var onIq = function(stanza) { |  | 
|  307     remoting.debug.logIq(false, stanza); |  | 
|  308     if (that.plugin.onIq) { |  | 
|  309       that.plugin.onIq(stanza); |  | 
|  310     } else { |  | 
|  311       // plugin.onIq may not be set after the plugin has been shut |  | 
|  312       // down. Particularly this happens when we receive response to |  | 
|  313       // session-terminate stanza. |  | 
|  314       remoting.debug.log( |  | 
|  315           'plugin.onIq is not set so dropping incoming message.'); |  | 
|  316     } |  | 
|  317   } |  | 
|  318   remoting.wcs.setOnIq(onIq); |  | 
|  319   that.plugin.connect(this.hostJid, this.hostPublicKey, this.clientJid, |  | 
|  320                       this.authenticationCode); |  | 
|  321 }; |  | 
|  322  |  | 
|  323 /** |  | 
|  324  * Callback that the plugin invokes to indicate that the connection |  | 
|  325  * status has changed. |  | 
|  326  * |  | 
|  327  * @param {number} status The plugin's status. |  | 
|  328  * @param {number} error The plugin's error state, if any. |  | 
|  329  */ |  | 
|  330 remoting.ClientSession.prototype.connectionInfoUpdateCallback = |  | 
|  331     function(status, error) { |  | 
|  332   // Old plugins didn't pass the status and error values, so get them directly. |  | 
|  333   // Note that there is a race condition inherent in this approach. |  | 
|  334   if (typeof(status) == 'undefined') { |  | 
|  335     status = this.plugin.status; |  | 
|  336   } |  | 
|  337   if (typeof(error) == 'undefined') { |  | 
|  338     error = this.plugin.error; |  | 
|  339   } |  | 
|  340  |  | 
|  341   if (status == this.plugin.STATUS_CONNECTED) { |  | 
|  342     this.onDesktopSizeChanged_(); |  | 
|  343   } else if (status == this.plugin.STATUS_FAILED) { |  | 
|  344     this.error = /** @type {remoting.ClientSession.ConnectionError} */ (error); |  | 
|  345   } |  | 
|  346   this.setState_(/** @type {remoting.ClientSession.State} */ (status)); |  | 
|  347 }; |  | 
|  348  |  | 
|  349 /** |  | 
|  350  * @private |  | 
|  351  * @param {remoting.ClientSession.State} newState The new state for the session. |  | 
|  352  * @return {void} Nothing. |  | 
|  353  */ |  | 
|  354 remoting.ClientSession.prototype.setState_ = function(newState) { |  | 
|  355   var oldState = this.state; |  | 
|  356   this.state = newState; |  | 
|  357   if (this.onStateChange) { |  | 
|  358     this.onStateChange(oldState, newState); |  | 
|  359   } |  | 
|  360   this.logToServer.logClientSessionStateChange(this.state, this.error); |  | 
|  361 }; |  | 
|  362  |  | 
|  363 /** |  | 
|  364  * This is a callback that gets called when the window is resized. |  | 
|  365  * |  | 
|  366  * @return {void} Nothing. |  | 
|  367  */ |  | 
|  368 remoting.ClientSession.prototype.onResize = function() { |  | 
|  369   this.updateDimensions(); |  | 
|  370 }; |  | 
|  371  |  | 
|  372 /** |  | 
|  373  * This is a callback that gets called when the plugin notifies us of a change |  | 
|  374  * in the size of the remote desktop. |  | 
|  375  * |  | 
|  376  * @private |  | 
|  377  * @return {void} Nothing. |  | 
|  378  */ |  | 
|  379 remoting.ClientSession.prototype.onDesktopSizeChanged_ = function() { |  | 
|  380   remoting.debug.log('desktop size changed: ' + |  | 
|  381                      this.plugin.desktopWidth + 'x' + |  | 
|  382                      this.plugin.desktopHeight); |  | 
|  383   this.updateDimensions(); |  | 
|  384 }; |  | 
|  385  |  | 
|  386 /** |  | 
|  387  * Refreshes the plugin's dimensions, taking into account the sizes of the |  | 
|  388  * remote desktop and client window, and the current scale-to-fit setting. |  | 
|  389  * |  | 
|  390  * @return {void} Nothing. |  | 
|  391  */ |  | 
|  392 remoting.ClientSession.prototype.updateDimensions = function() { |  | 
|  393   if (this.plugin.desktopWidth == 0 || |  | 
|  394       this.plugin.desktopHeight == 0) |  | 
|  395     return; |  | 
|  396  |  | 
|  397   var windowWidth = window.innerWidth; |  | 
|  398   var windowHeight = window.innerHeight; |  | 
|  399   var scale = 1.0; |  | 
|  400  |  | 
|  401   if (remoting.scaleToFit) { |  | 
|  402     var scaleFitHeight = 1.0 * windowHeight / this.plugin.desktopHeight; |  | 
|  403     var scaleFitWidth = 1.0 * windowWidth / this.plugin.desktopWidth; |  | 
|  404     scale = Math.min(1.0, scaleFitHeight, scaleFitWidth); |  | 
|  405   } |  | 
|  406  |  | 
|  407   // Resize the plugin if necessary. |  | 
|  408   this.plugin.width = this.plugin.desktopWidth * scale; |  | 
|  409   this.plugin.height = this.plugin.desktopHeight * scale; |  | 
|  410  |  | 
|  411   // Position the container. |  | 
|  412   // TODO(wez): We should take into account scrollbars when positioning. |  | 
|  413   var parentNode = this.plugin.parentNode; |  | 
|  414   if (this.plugin.width < windowWidth) |  | 
|  415     parentNode.style.left = (windowWidth - this.plugin.width) / 2 + 'px'; |  | 
|  416   else |  | 
|  417     parentNode.style.left = '0'; |  | 
|  418   if (this.plugin.height < windowHeight) |  | 
|  419     parentNode.style.top = (windowHeight - this.plugin.height) / 2 + 'px'; |  | 
|  420   else |  | 
|  421     parentNode.style.top = '0'; |  | 
|  422  |  | 
|  423   remoting.debug.log('plugin dimensions: ' + |  | 
|  424                      parentNode.style.left + ',' + |  | 
|  425                      parentNode.style.top + '-' + |  | 
|  426                      this.plugin.width + 'x' + this.plugin.height + '.'); |  | 
|  427   this.plugin.setScaleToFit(remoting.scaleToFit); |  | 
|  428 }; |  | 
|  429  |  | 
|  430 /** |  | 
|  431  * Returns an associative array with a set of stats for this connection. |  | 
|  432  * |  | 
|  433  * @return {Object.<string, number>} The connection statistics. |  | 
|  434  */ |  | 
|  435 remoting.ClientSession.prototype.stats = function() { |  | 
|  436   var dict = {}; |  | 
|  437   dict[remoting.ClientSession.STATS_KEY_VIDEO_BANDWIDTH] = |  | 
|  438       this.plugin.videoBandwidth; |  | 
|  439   dict[remoting.ClientSession.STATS_KEY_VIDEO_FRAME_RATE] = |  | 
|  440       this.plugin.videoFrameRate; |  | 
|  441   dict[remoting.ClientSession.STATS_KEY_CAPTURE_LATENCY] = |  | 
|  442       this.plugin.videoCaptureLatency; |  | 
|  443   dict[remoting.ClientSession.STATS_KEY_ENCODE_LATENCY] = |  | 
|  444       this.plugin.videoEncodeLatency; |  | 
|  445   dict[remoting.ClientSession.STATS_KEY_DECODE_LATENCY] = |  | 
|  446       this.plugin.videoDecodeLatency; |  | 
|  447   dict[remoting.ClientSession.STATS_KEY_RENDER_LATENCY] = |  | 
|  448       this.plugin.videoRenderLatency; |  | 
|  449   dict[remoting.ClientSession.STATS_KEY_ROUNDTRIP_LATENCY] = |  | 
|  450       this.plugin.roundTripLatency; |  | 
|  451   return dict; |  | 
|  452 }; |  | 
|  453  |  | 
|  454 /** |  | 
|  455  * Logs statistics. |  | 
|  456  * |  | 
|  457  * @param {Object.<string, number>} stats |  | 
|  458  */ |  | 
|  459 remoting.ClientSession.prototype.logStatistics = function(stats) { |  | 
|  460   this.logToServer.logStatistics(stats); |  | 
|  461 }; |  | 
| OLD | NEW |