Home | History | Annotate | Download | only in webapp
      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  * The ClientSession class controls lifetime of the client plugin
     10  * object and provides the plugin with the functionality it needs to
     11  * establish connection. Specifically it:
     12  *  - Delivers incoming/outgoing signaling messages,
     13  *  - Adjusts plugin size and position when destop resolution changes,
     14  *
     15  * This class should not access the plugin directly, instead it should
     16  * do it through ClientPlugin class which abstracts plugin version
     17  * differences.
     18  */
     19 
     20 'use strict';
     21 
     22 /** @suppress {duplicate} */
     23 var remoting = remoting || {};
     24 
     25 /**
     26  * True if Cast capability is supported.
     27  *
     28  * @type {boolean}
     29  */
     30 remoting.enableCast = false;
     31 
     32 /**
     33  * @param {remoting.SignalStrategy} signalStrategy Signal strategy.
     34  * @param {HTMLElement} container Container element for the client view.
     35  * @param {string} hostDisplayName A human-readable name for the host.
     36  * @param {string} accessCode The IT2Me access code. Blank for Me2Me.
     37  * @param {function(boolean, function(string): void): void} fetchPin
     38  *     Called by Me2Me connections when a PIN needs to be obtained
     39  *     interactively.
     40  * @param {function(string, string, string,
     41  *                  function(string, string): void): void}
     42  *     fetchThirdPartyToken Called by Me2Me connections when a third party
     43  *     authentication token must be obtained.
     44  * @param {string} authenticationMethods Comma-separated list of
     45  *     authentication methods the client should attempt to use.
     46  * @param {string} hostId The host identifier for Me2Me, or empty for IT2Me.
     47  *     Mixed into authentication hashes for some authentication methods.
     48  * @param {string} hostJid The jid of the host to connect to.
     49  * @param {string} hostPublicKey The base64 encoded version of the host's
     50  *     public key.
     51  * @param {remoting.ClientSession.Mode} mode The mode of this connection.
     52  * @param {string} clientPairingId For paired Me2Me connections, the
     53  *     pairing id for this client, as issued by the host.
     54  * @param {string} clientPairedSecret For paired Me2Me connections, the
     55  *     paired secret for this client, as issued by the host.
     56  * @constructor
     57  * @extends {base.EventSource}
     58  */
     59 remoting.ClientSession = function(signalStrategy, container, hostDisplayName,
     60                                   accessCode, fetchPin, fetchThirdPartyToken,
     61                                   authenticationMethods, hostId, hostJid,
     62                                   hostPublicKey, mode, clientPairingId,
     63                                   clientPairedSecret) {
     64   /** @private */
     65   this.state_ = remoting.ClientSession.State.CREATED;
     66 
     67   /** @private */
     68   this.error_ = remoting.Error.NONE;
     69 
     70   /** @type {HTMLElement}
     71     * @private */
     72   this.container_ = container;
     73 
     74   /** @private */
     75   this.hostDisplayName_ = hostDisplayName;
     76   /** @private */
     77   this.hostJid_ = hostJid;
     78   /** @private */
     79   this.hostPublicKey_ = hostPublicKey;
     80   /** @private */
     81   this.accessCode_ = accessCode;
     82   /** @private */
     83   this.fetchPin_ = fetchPin;
     84   /** @private */
     85   this.fetchThirdPartyToken_ = fetchThirdPartyToken;
     86   /** @private */
     87   this.authenticationMethods_ = authenticationMethods;
     88   /** @private */
     89   this.hostId_ = hostId;
     90   /** @private */
     91   this.mode_ = mode;
     92   /** @private */
     93   this.clientPairingId_ = clientPairingId;
     94   /** @private */
     95   this.clientPairedSecret_ = clientPairedSecret;
     96   /** @private */
     97   this.sessionId_ = '';
     98   /** @type {remoting.ClientPlugin}
     99     * @private */
    100   this.plugin_ = null;
    101   /** @private */
    102   this.shrinkToFit_ = true;
    103   /** @private */
    104   this.resizeToClient_ = true;
    105   /** @private */
    106   this.remapKeys_ = '';
    107   /** @private */
    108   this.hasReceivedFrame_ = false;
    109   this.logToServer = new remoting.LogToServer();
    110 
    111   /** @private */
    112   this.signalStrategy_ = signalStrategy;
    113   base.debug.assert(this.signalStrategy_.getState() ==
    114                     remoting.SignalStrategy.State.CONNECTED);
    115   this.signalStrategy_.setIncomingStanzaCallback(
    116       this.onIncomingMessage_.bind(this));
    117   remoting.formatIq.setJids(this.signalStrategy_.getJid(), hostJid);
    118 
    119   /** @type {number?} @private */
    120   this.notifyClientResolutionTimer_ = null;
    121   /** @type {number?} @private */
    122   this.bumpScrollTimer_ = null;
    123 
    124   // Bump-scroll test variables. Override to use a fake value for the width
    125   // and height of the client plugin so that bump-scrolling can be tested
    126   // without relying on the actual size of the host desktop.
    127   /** @type {number} @private */
    128   this.pluginWidthForBumpScrollTesting = 0;
    129   /** @type {number} @private */
    130   this.pluginHeightForBumpScrollTesting = 0;
    131 
    132   /**
    133    * Allow host-offline error reporting to be suppressed in situations where it
    134    * would not be useful, for example, when using a cached host JID.
    135    *
    136    * @type {boolean} @private
    137    */
    138   this.logHostOfflineErrors_ = true;
    139 
    140   /** @private */
    141   this.callPluginLostFocus_ = this.pluginLostFocus_.bind(this);
    142   /** @private */
    143   this.callPluginGotFocus_ = this.pluginGotFocus_.bind(this);
    144   /** @private */
    145   this.callOnFullScreenChanged_ = this.onFullScreenChanged_.bind(this)
    146 
    147   /** @type {HTMLMediaElement} @private */
    148   this.video_ = null;
    149 
    150   /** @type {Element} @private */
    151   this.mouseCursorOverlay_ =
    152       this.container_.querySelector('.mouse-cursor-overlay');
    153 
    154   /** @type {Element} */
    155   var img = this.mouseCursorOverlay_;
    156   /** @param {Event} event @private */
    157   this.updateMouseCursorPosition_ = function(event) {
    158     img.style.top = event.y + 'px';
    159     img.style.left = event.x + 'px';
    160   };
    161 
    162   /** @type {remoting.GnubbyAuthHandler} @private */
    163   this.gnubbyAuthHandler_ = null;
    164 
    165   /** @type {remoting.CastExtensionHandler} @private */
    166   this.castExtensionHandler_ = null;
    167 
    168   /** @type {remoting.VideoFrameRecorder} @private */
    169   this.videoFrameRecorder_ = null;
    170 
    171   this.defineEvents(Object.keys(remoting.ClientSession.Events));
    172 };
    173 
    174 base.extend(remoting.ClientSession, base.EventSource);
    175 
    176 /** @enum {string} */
    177 remoting.ClientSession.Events = {
    178   stateChanged: 'stateChanged',
    179   videoChannelStateChanged: 'videoChannelStateChanged',
    180   bumpScrollStarted: 'bumpScrollStarted',
    181   bumpScrollStopped: 'bumpScrollStopped'
    182 };
    183 
    184 /**
    185  * Get host display name.
    186  *
    187  * @return {string}
    188  */
    189 remoting.ClientSession.prototype.getHostDisplayName = function() {
    190   return this.hostDisplayName_;
    191 };
    192 
    193 /**
    194  * Called when the window or desktop size or the scaling settings change,
    195  * to set the scroll-bar visibility.
    196  *
    197  * TODO(jamiewalch): crbug.com/252796: Remove this once crbug.com/240772 is
    198  * fixed.
    199  */
    200 remoting.ClientSession.prototype.updateScrollbarVisibility = function() {
    201   var needsVerticalScroll = false;
    202   var needsHorizontalScroll = false;
    203   if (!this.shrinkToFit_) {
    204     // Determine whether or not horizontal or vertical scrollbars are
    205     // required, taking into account their width.
    206     var clientArea = this.getClientArea_();
    207     needsVerticalScroll = clientArea.height < this.plugin_.getDesktopHeight();
    208     needsHorizontalScroll = clientArea.width < this.plugin_.getDesktopWidth();
    209     var kScrollBarWidth = 16;
    210     if (needsHorizontalScroll && !needsVerticalScroll) {
    211       needsVerticalScroll =
    212           clientArea.height - kScrollBarWidth < this.plugin_.getDesktopHeight();
    213     } else if (!needsHorizontalScroll && needsVerticalScroll) {
    214       needsHorizontalScroll =
    215           clientArea.width - kScrollBarWidth < this.plugin_.getDesktopWidth();
    216     }
    217   }
    218 
    219   var scroller = document.getElementById('scroller');
    220   if (needsHorizontalScroll) {
    221     scroller.classList.remove('no-horizontal-scroll');
    222   } else {
    223     scroller.classList.add('no-horizontal-scroll');
    224   }
    225   if (needsVerticalScroll) {
    226     scroller.classList.remove('no-vertical-scroll');
    227   } else {
    228     scroller.classList.add('no-vertical-scroll');
    229   }
    230 };
    231 
    232 /**
    233  * @return {boolean} True if shrink-to-fit is enabled; false otherwise.
    234  */
    235 remoting.ClientSession.prototype.getShrinkToFit = function() {
    236   return this.shrinkToFit_;
    237 };
    238 
    239 /**
    240  * @return {boolean} True if resize-to-client is enabled; false otherwise.
    241  */
    242 remoting.ClientSession.prototype.getResizeToClient = function() {
    243   return this.resizeToClient_;
    244 };
    245 
    246 // Note that the positive values in both of these enums are copied directly
    247 // from chromoting_scriptable_object.h and must be kept in sync. The negative
    248 // values represent state transitions that occur within the web-app that have
    249 // no corresponding plugin state transition.
    250 /** @enum {number} */
    251 remoting.ClientSession.State = {
    252   CONNECTION_CANCELED: -3,  // Connection closed (gracefully) before connecting.
    253   CONNECTION_DROPPED: -2,  // Succeeded, but subsequently closed with an error.
    254   CREATED: -1,
    255   UNKNOWN: 0,
    256   CONNECTING: 1,
    257   INITIALIZING: 2,
    258   CONNECTED: 3,
    259   CLOSED: 4,
    260   FAILED: 5
    261 };
    262 
    263 /**
    264  * @param {string} state The state name.
    265  * @return {remoting.ClientSession.State} The session state enum value.
    266  */
    267 remoting.ClientSession.State.fromString = function(state) {
    268   if (!remoting.ClientSession.State.hasOwnProperty(state)) {
    269     throw "Invalid ClientSession.State: " + state;
    270   }
    271   return remoting.ClientSession.State[state];
    272 };
    273 
    274 /**
    275   @constructor
    276   @param {remoting.ClientSession.State} current
    277   @param {remoting.ClientSession.State} previous
    278 */
    279 remoting.ClientSession.StateEvent = function(current, previous) {
    280   /** @type {remoting.ClientSession.State} */
    281   this.previous = previous
    282 
    283   /** @type {remoting.ClientSession.State} */
    284   this.current = current;
    285 };
    286 
    287 /** @enum {number} */
    288 remoting.ClientSession.ConnectionError = {
    289   UNKNOWN: -1,
    290   NONE: 0,
    291   HOST_IS_OFFLINE: 1,
    292   SESSION_REJECTED: 2,
    293   INCOMPATIBLE_PROTOCOL: 3,
    294   NETWORK_FAILURE: 4,
    295   HOST_OVERLOAD: 5
    296 };
    297 
    298 /**
    299  * @param {string} error The connection error name.
    300  * @return {remoting.ClientSession.ConnectionError} The connection error enum.
    301  */
    302 remoting.ClientSession.ConnectionError.fromString = function(error) {
    303   if (!remoting.ClientSession.ConnectionError.hasOwnProperty(error)) {
    304     console.error('Unexpected ClientSession.ConnectionError string: ', error);
    305     return remoting.ClientSession.ConnectionError.UNKNOWN;
    306   }
    307   return remoting.ClientSession.ConnectionError[error];
    308 }
    309 
    310 // The mode of this session.
    311 /** @enum {number} */
    312 remoting.ClientSession.Mode = {
    313   IT2ME: 0,
    314   ME2ME: 1
    315 };
    316 
    317 /**
    318  * Type used for performance statistics collected by the plugin.
    319  * @constructor
    320  */
    321 remoting.ClientSession.PerfStats = function() {};
    322 /** @type {number} */
    323 remoting.ClientSession.PerfStats.prototype.videoBandwidth;
    324 /** @type {number} */
    325 remoting.ClientSession.PerfStats.prototype.videoFrameRate;
    326 /** @type {number} */
    327 remoting.ClientSession.PerfStats.prototype.captureLatency;
    328 /** @type {number} */
    329 remoting.ClientSession.PerfStats.prototype.encodeLatency;
    330 /** @type {number} */
    331 remoting.ClientSession.PerfStats.prototype.decodeLatency;
    332 /** @type {number} */
    333 remoting.ClientSession.PerfStats.prototype.renderLatency;
    334 /** @type {number} */
    335 remoting.ClientSession.PerfStats.prototype.roundtripLatency;
    336 
    337 // Keys for connection statistics.
    338 remoting.ClientSession.STATS_KEY_VIDEO_BANDWIDTH = 'videoBandwidth';
    339 remoting.ClientSession.STATS_KEY_VIDEO_FRAME_RATE = 'videoFrameRate';
    340 remoting.ClientSession.STATS_KEY_CAPTURE_LATENCY = 'captureLatency';
    341 remoting.ClientSession.STATS_KEY_ENCODE_LATENCY = 'encodeLatency';
    342 remoting.ClientSession.STATS_KEY_DECODE_LATENCY = 'decodeLatency';
    343 remoting.ClientSession.STATS_KEY_RENDER_LATENCY = 'renderLatency';
    344 remoting.ClientSession.STATS_KEY_ROUNDTRIP_LATENCY = 'roundtripLatency';
    345 
    346 // Keys for per-host settings.
    347 remoting.ClientSession.KEY_REMAP_KEYS = 'remapKeys';
    348 remoting.ClientSession.KEY_RESIZE_TO_CLIENT = 'resizeToClient';
    349 remoting.ClientSession.KEY_SHRINK_TO_FIT = 'shrinkToFit';
    350 
    351 /**
    352  * Set of capabilities for which hasCapability_() can be used to test.
    353  *
    354  * @enum {string}
    355  */
    356 remoting.ClientSession.Capability = {
    357   // When enabled this capability causes the client to send its screen
    358   // resolution to the host once connection has been established. See
    359   // this.plugin_.notifyClientResolution().
    360   SEND_INITIAL_RESOLUTION: 'sendInitialResolution',
    361   RATE_LIMIT_RESIZE_REQUESTS: 'rateLimitResizeRequests',
    362   VIDEO_RECORDER: 'videoRecorder',
    363   CAST: 'casting'
    364 };
    365 
    366 /**
    367  * The set of capabilities negotiated between the client and host.
    368  * @type {Array.<string>}
    369  * @private
    370  */
    371 remoting.ClientSession.prototype.capabilities_ = null;
    372 
    373 /**
    374  * @param {remoting.ClientSession.Capability} capability The capability to test
    375  *     for.
    376  * @return {boolean} True if the capability has been negotiated between
    377  *     the client and host.
    378  * @private
    379  */
    380 remoting.ClientSession.prototype.hasCapability_ = function(capability) {
    381   if (this.capabilities_ == null)
    382     return false;
    383 
    384   return this.capabilities_.indexOf(capability) > -1;
    385 };
    386 
    387 /**
    388  * Callback function called when the plugin element gets focus.
    389  */
    390 remoting.ClientSession.prototype.pluginGotFocus_ = function() {
    391   remoting.clipboard.initiateToHost();
    392 };
    393 
    394 /**
    395  * Callback function called when the plugin element loses focus.
    396  */
    397 remoting.ClientSession.prototype.pluginLostFocus_ = function() {
    398   if (this.plugin_) {
    399     // Release all keys to prevent them becoming 'stuck down' on the host.
    400     this.plugin_.releaseAllKeys();
    401     if (this.plugin_.element()) {
    402       // Focus should stay on the element, not (for example) the toolbar.
    403       // Due to crbug.com/246335, we can't restore the focus immediately,
    404       // otherwise the plugin gets confused about whether or not it has focus.
    405       window.setTimeout(
    406           this.plugin_.element().focus.bind(this.plugin_.element()), 0);
    407     }
    408   }
    409 };
    410 
    411 /**
    412  * Adds <embed> element to |container| and readies the sesion object.
    413  *
    414  * @param {function(string, string):boolean} onExtensionMessage The handler for
    415  *     protocol extension messages. Returns true if a message is recognized;
    416  *     false otherwise.
    417  */
    418 remoting.ClientSession.prototype.createPluginAndConnect =
    419     function(onExtensionMessage) {
    420   this.plugin_ = remoting.ClientPlugin.factory.createPlugin(
    421       this.container_.querySelector('.client-plugin-container'),
    422       onExtensionMessage);
    423   remoting.HostSettings.load(this.hostId_,
    424                              this.onHostSettingsLoaded_.bind(this));
    425 };
    426 
    427 /**
    428  * @param {Object.<string>} options The current options for the host, or {}
    429  *     if this client has no saved settings for the host.
    430  * @private
    431  */
    432 remoting.ClientSession.prototype.onHostSettingsLoaded_ = function(options) {
    433   if (remoting.ClientSession.KEY_REMAP_KEYS in options &&
    434       typeof(options[remoting.ClientSession.KEY_REMAP_KEYS]) ==
    435           'string') {
    436     this.remapKeys_ = /** @type {string} */
    437         options[remoting.ClientSession.KEY_REMAP_KEYS];
    438   }
    439   if (remoting.ClientSession.KEY_RESIZE_TO_CLIENT in options &&
    440       typeof(options[remoting.ClientSession.KEY_RESIZE_TO_CLIENT]) ==
    441           'boolean') {
    442     this.resizeToClient_ = /** @type {boolean} */
    443         options[remoting.ClientSession.KEY_RESIZE_TO_CLIENT];
    444   }
    445   if (remoting.ClientSession.KEY_SHRINK_TO_FIT in options &&
    446       typeof(options[remoting.ClientSession.KEY_SHRINK_TO_FIT]) ==
    447           'boolean') {
    448     this.shrinkToFit_ = /** @type {boolean} */
    449         options[remoting.ClientSession.KEY_SHRINK_TO_FIT];
    450   }
    451 
    452   /** @param {boolean} result */
    453   this.plugin_.initialize(this.onPluginInitialized_.bind(this));
    454 };
    455 
    456 /**
    457  * Constrains the focus to the plugin element.
    458  * @private
    459  */
    460 remoting.ClientSession.prototype.setFocusHandlers_ = function() {
    461   this.plugin_.element().addEventListener(
    462       'focus', this.callPluginGotFocus_, false);
    463   this.plugin_.element().addEventListener(
    464       'blur', this.callPluginLostFocus_, false);
    465   this.plugin_.element().focus();
    466 };
    467 
    468 /**
    469  * @param {remoting.Error} error
    470  */
    471 remoting.ClientSession.prototype.resetWithError_ = function(error) {
    472   this.signalStrategy_.setIncomingStanzaCallback(null);
    473   this.plugin_.dispose();
    474   this.plugin_ = null;
    475   this.error_ = error;
    476   this.setState_(remoting.ClientSession.State.FAILED);
    477 }
    478 
    479 /**
    480  * @param {boolean} initialized
    481  */
    482 remoting.ClientSession.prototype.onPluginInitialized_ = function(initialized) {
    483   if (!initialized) {
    484     console.error('ERROR: remoting plugin not loaded');
    485     this.resetWithError_(remoting.Error.MISSING_PLUGIN);
    486     return;
    487   }
    488 
    489   if (!this.plugin_.isSupportedVersion()) {
    490     this.resetWithError_(remoting.Error.BAD_PLUGIN_VERSION);
    491     return;
    492   }
    493 
    494   // Show the Send Keys menu only if the plugin has the injectKeyEvent feature,
    495   // and the Ctrl-Alt-Del button only in Me2Me mode.
    496   if (!this.plugin_.hasFeature(
    497           remoting.ClientPlugin.Feature.INJECT_KEY_EVENT)) {
    498     var sendKeysElement = document.getElementById('send-keys-menu');
    499     sendKeysElement.hidden = true;
    500   } else if (this.mode_ != remoting.ClientSession.Mode.ME2ME) {
    501     var sendCadElement = document.getElementById('send-ctrl-alt-del');
    502     sendCadElement.hidden = true;
    503   }
    504 
    505   // Apply customized key remappings if the plugin supports remapKeys.
    506   if (this.plugin_.hasFeature(remoting.ClientPlugin.Feature.REMAP_KEY)) {
    507     this.applyRemapKeys_(true);
    508   }
    509 
    510   // Enable MediaSource-based rendering on Chrome 37 and above.
    511   var chromeVersionMajor =
    512       parseInt((remoting.getChromeVersion() || '0').split('.')[0], 10);
    513   if (chromeVersionMajor >= 37 &&
    514       this.plugin_.hasFeature(
    515           remoting.ClientPlugin.Feature.MEDIA_SOURCE_RENDERING)) {
    516     this.video_ = /** @type {HTMLMediaElement} */(
    517         this.container_.querySelector('video'));
    518     // Make sure that the <video> element is hidden until we get the first
    519     // frame.
    520     this.video_.style.width = '0px';
    521     this.video_.style.height = '0px';
    522 
    523     var renderer = new remoting.MediaSourceRenderer(this.video_);
    524     this.plugin_.enableMediaSourceRendering(renderer);
    525     this.container_.classList.add('mediasource-rendering');
    526   } else {
    527     this.container_.classList.remove('mediasource-rendering');
    528   }
    529 
    530   this.plugin_.setOnOutgoingIqHandler(this.sendIq_.bind(this));
    531   this.plugin_.setOnDebugMessageHandler(
    532       /** @param {string} msg */
    533       function(msg) {
    534         console.log('plugin: ' + msg.trimRight());
    535       });
    536 
    537   this.plugin_.setConnectionStatusUpdateHandler(
    538       this.onConnectionStatusUpdate_.bind(this));
    539   this.plugin_.setConnectionReadyHandler(this.onConnectionReady_.bind(this));
    540   this.plugin_.setDesktopSizeUpdateHandler(
    541       this.onDesktopSizeChanged_.bind(this));
    542   this.plugin_.setCapabilitiesHandler(this.onSetCapabilities_.bind(this));
    543   this.plugin_.setGnubbyAuthHandler(
    544       this.processGnubbyAuthMessage_.bind(this));
    545   this.plugin_.setMouseCursorHandler(this.updateMouseCursorImage_.bind(this));
    546   this.plugin_.setCastExtensionHandler(
    547       this.processCastExtensionMessage_.bind(this));
    548   this.initiateConnection_();
    549 };
    550 
    551 /**
    552  * Deletes the <embed> element from the container, without sending a
    553  * session_terminate request.  This is to be called when the session was
    554  * disconnected by the Host.
    555  *
    556  * @return {void} Nothing.
    557  */
    558 remoting.ClientSession.prototype.removePlugin = function() {
    559   if (this.plugin_) {
    560     this.plugin_.element().removeEventListener(
    561         'focus', this.callPluginGotFocus_, false);
    562     this.plugin_.element().removeEventListener(
    563         'blur', this.callPluginLostFocus_, false);
    564     this.plugin_.dispose();
    565     this.plugin_ = null;
    566   }
    567 
    568   // Leave full-screen mode, and stop listening for related events.
    569   var listener = this.callOnFullScreenChanged_;
    570   remoting.fullscreen.activate(
    571       false,
    572       function() {
    573         remoting.fullscreen.removeListener(listener);
    574       });
    575   if (remoting.windowFrame) {
    576     remoting.windowFrame.setClientSession(null);
    577   } else {
    578     remoting.toolbar.setClientSession(null);
    579   }
    580   remoting.optionsMenu.setClientSession(null);
    581   document.body.classList.remove('connected');
    582 
    583   // Remove mediasource-rendering class from the container - this will also
    584   // hide the <video> element.
    585   this.container_.classList.remove('mediasource-rendering');
    586 
    587   this.container_.removeEventListener('mousemove',
    588                                       this.updateMouseCursorPosition_,
    589                                       true);
    590 };
    591 
    592 /**
    593  * Disconnect the current session with a particular |error|.  The session will
    594  * raise a |stateChanged| event in response to it.  The caller should then call
    595  * |cleanup| to remove and destroy the <embed> element.
    596  *
    597  * @param {remoting.Error} error The reason for the disconnection.  Use
    598  *    remoting.Error.NONE if there is no error.
    599  * @return {void} Nothing.
    600  */
    601 remoting.ClientSession.prototype.disconnect = function(error) {
    602   var state = (error == remoting.Error.NONE) ?
    603                   remoting.ClientSession.State.CLOSED :
    604                   remoting.ClientSession.State.FAILED;
    605 
    606   // The plugin won't send a state change notification, so we explicitly log
    607   // the fact that the connection has closed.
    608   this.logToServer.logClientSessionStateChange(state, error, this.mode_);
    609   this.error_ = error;
    610   this.setState_(state);
    611 };
    612 
    613 /**
    614  * Deletes the <embed> element from the container and disconnects.
    615  *
    616  * @return {void} Nothing.
    617  */
    618 remoting.ClientSession.prototype.cleanup = function() {
    619   this.sendIq_(
    620       '<cli:iq ' +
    621           'to="' + this.hostJid_ + '" ' +
    622           'type="set" ' +
    623           'id="session-terminate" ' +
    624           'xmlns:cli="jabber:client">' +
    625         '<jingle ' +
    626             'xmlns="urn:xmpp:jingle:1" ' +
    627             'action="session-terminate" ' +
    628             'sid="' + this.sessionId_ + '">' +
    629           '<reason><success/></reason>' +
    630         '</jingle>' +
    631       '</cli:iq>');
    632   this.removePlugin();
    633 };
    634 
    635 /**
    636  * @return {remoting.ClientSession.Mode} The current state.
    637  */
    638 remoting.ClientSession.prototype.getMode = function() {
    639   return this.mode_;
    640 };
    641 
    642 /**
    643  * @return {remoting.ClientSession.State} The current state.
    644  */
    645 remoting.ClientSession.prototype.getState = function() {
    646   return this.state_;
    647 };
    648 
    649 /**
    650  * @return {remoting.Error} The current error code.
    651  */
    652 remoting.ClientSession.prototype.getError = function() {
    653   return this.error_;
    654 };
    655 
    656 /**
    657  * Sends a key combination to the remoting client, by sending down events for
    658  * the given keys, followed by up events in reverse order.
    659  *
    660  * @private
    661  * @param {[number]} keys Key codes to be sent.
    662  * @return {void} Nothing.
    663  */
    664 remoting.ClientSession.prototype.sendKeyCombination_ = function(keys) {
    665   for (var i = 0; i < keys.length; i++) {
    666     this.plugin_.injectKeyEvent(keys[i], true);
    667   }
    668   for (var i = 0; i < keys.length; i++) {
    669     this.plugin_.injectKeyEvent(keys[i], false);
    670   }
    671 }
    672 
    673 /**
    674  * Sends a Ctrl-Alt-Del sequence to the remoting client.
    675  *
    676  * @return {void} Nothing.
    677  */
    678 remoting.ClientSession.prototype.sendCtrlAltDel = function() {
    679   console.log('Sending Ctrl-Alt-Del.');
    680   this.sendKeyCombination_([0x0700e0, 0x0700e2, 0x07004c]);
    681 }
    682 
    683 /**
    684  * Sends a Print Screen keypress to the remoting client.
    685  *
    686  * @return {void} Nothing.
    687  */
    688 remoting.ClientSession.prototype.sendPrintScreen = function() {
    689   console.log('Sending Print Screen.');
    690   this.sendKeyCombination_([0x070046]);
    691 }
    692 
    693 /**
    694  * Sets and stores the key remapping setting for the current host.
    695  *
    696  * @param {string} remappings Comma separated list of key remappings.
    697  */
    698 remoting.ClientSession.prototype.setRemapKeys = function(remappings) {
    699   // Cancel any existing remappings and apply the new ones.
    700   this.applyRemapKeys_(false);
    701   this.remapKeys_ = remappings;
    702   this.applyRemapKeys_(true);
    703 
    704   // Save the new remapping setting.
    705   var options = {};
    706   options[remoting.ClientSession.KEY_REMAP_KEYS] = this.remapKeys_;
    707   remoting.HostSettings.save(this.hostId_, options);
    708 }
    709 
    710 /**
    711  * Applies the configured key remappings to the session, or resets them.
    712  *
    713  * @param {boolean} apply True to apply remappings, false to cancel them.
    714  */
    715 remoting.ClientSession.prototype.applyRemapKeys_ = function(apply) {
    716   // By default, under ChromeOS, remap the right Control key to the right
    717   // Win / Cmd key.
    718   var remapKeys = this.remapKeys_;
    719   if (remapKeys == '' && remoting.runningOnChromeOS()) {
    720     remapKeys = '0x0700e4>0x0700e7';
    721   }
    722 
    723   if (remapKeys == '') {
    724     return;
    725   }
    726 
    727   var remappings = remapKeys.split(',');
    728   for (var i = 0; i < remappings.length; ++i) {
    729     var keyCodes = remappings[i].split('>');
    730     if (keyCodes.length != 2) {
    731       console.log('bad remapKey: ' + remappings[i]);
    732       continue;
    733     }
    734     var fromKey = parseInt(keyCodes[0], 0);
    735     var toKey = parseInt(keyCodes[1], 0);
    736     if (!fromKey || !toKey) {
    737       console.log('bad remapKey code: ' + remappings[i]);
    738       continue;
    739     }
    740     if (apply) {
    741       console.log('remapKey 0x' + fromKey.toString(16) +
    742                   '>0x' + toKey.toString(16));
    743       this.plugin_.remapKey(fromKey, toKey);
    744     } else {
    745       console.log('cancel remapKey 0x' + fromKey.toString(16));
    746       this.plugin_.remapKey(fromKey, fromKey);
    747     }
    748   }
    749 }
    750 
    751 /**
    752  * Set the shrink-to-fit and resize-to-client flags and save them if this is
    753  * a Me2Me connection.
    754  *
    755  * @param {boolean} shrinkToFit True if the remote desktop should be scaled
    756  *     down if it is larger than the client window; false if scroll-bars
    757  *     should be added in this case.
    758  * @param {boolean} resizeToClient True if window resizes should cause the
    759  *     host to attempt to resize its desktop to match the client window size;
    760  *     false to disable this behaviour for subsequent window resizes--the
    761  *     current host desktop size is not restored in this case.
    762  * @return {void} Nothing.
    763  */
    764 remoting.ClientSession.prototype.setScreenMode =
    765     function(shrinkToFit, resizeToClient) {
    766   if (resizeToClient && !this.resizeToClient_) {
    767     var clientArea = this.getClientArea_();
    768     this.plugin_.notifyClientResolution(clientArea.width,
    769                                         clientArea.height,
    770                                         window.devicePixelRatio);
    771   }
    772 
    773   // If enabling shrink, reset bump-scroll offsets.
    774   var needsScrollReset = shrinkToFit && !this.shrinkToFit_;
    775 
    776   this.shrinkToFit_ = shrinkToFit;
    777   this.resizeToClient_ = resizeToClient;
    778   this.updateScrollbarVisibility();
    779 
    780   if (this.hostId_ != '') {
    781     var options = {};
    782     options[remoting.ClientSession.KEY_SHRINK_TO_FIT] = this.shrinkToFit_;
    783     options[remoting.ClientSession.KEY_RESIZE_TO_CLIENT] = this.resizeToClient_;
    784     remoting.HostSettings.save(this.hostId_, options);
    785   }
    786 
    787   this.updateDimensions();
    788   if (needsScrollReset) {
    789     this.resetScroll_();
    790   }
    791 
    792 }
    793 
    794 /**
    795  * Called when the client receives its first frame.
    796  *
    797  * @return {void} Nothing.
    798  */
    799 remoting.ClientSession.prototype.onFirstFrameReceived = function() {
    800   this.hasReceivedFrame_ = true;
    801 };
    802 
    803 /**
    804  * @return {boolean} Whether the client has received a video buffer.
    805  */
    806 remoting.ClientSession.prototype.hasReceivedFrame = function() {
    807   return this.hasReceivedFrame_;
    808 };
    809 
    810 /**
    811  * Sends a signaling message.
    812  *
    813  * @private
    814  * @param {string} message XML string of IQ stanza to send to server.
    815  * @return {void} Nothing.
    816  */
    817 remoting.ClientSession.prototype.sendIq_ = function(message) {
    818   // Extract the session id, so we can close the session later.
    819   var parser = new DOMParser();
    820   var iqNode = parser.parseFromString(message, 'text/xml').firstChild;
    821   var jingleNode = iqNode.firstChild;
    822   if (jingleNode) {
    823     var action = jingleNode.getAttribute('action');
    824     if (jingleNode.nodeName == 'jingle' && action == 'session-initiate') {
    825       this.sessionId_ = jingleNode.getAttribute('sid');
    826     }
    827   }
    828 
    829   console.log(remoting.timestamp(), remoting.formatIq.prettifySendIq(message));
    830   if (this.signalStrategy_.getState() !=
    831       remoting.SignalStrategy.State.CONNECTED) {
    832     console.log("Message above is dropped because signaling is not connected.");
    833     return;
    834   }
    835 
    836   this.signalStrategy_.sendMessage(message);
    837 };
    838 
    839 /**
    840  * @private
    841  * @param {Element} message
    842  */
    843 remoting.ClientSession.prototype.onIncomingMessage_ = function(message) {
    844   if (!this.plugin_) {
    845     return;
    846   }
    847   var formatted = new XMLSerializer().serializeToString(message);
    848   console.log(remoting.timestamp(),
    849               remoting.formatIq.prettifyReceiveIq(formatted));
    850   this.plugin_.onIncomingIq(formatted);
    851 }
    852 
    853 /**
    854  * @private
    855  */
    856 remoting.ClientSession.prototype.initiateConnection_ = function() {
    857   /** @type {remoting.ClientSession} */
    858   var that = this;
    859 
    860   /** @param {string} sharedSecret Shared secret. */
    861   function onSharedSecretReceived(sharedSecret) {
    862     that.plugin_.connect(
    863         that.hostJid_, that.hostPublicKey_, that.signalStrategy_.getJid(),
    864         sharedSecret, that.authenticationMethods_, that.hostId_,
    865         that.clientPairingId_, that.clientPairedSecret_);
    866   };
    867 
    868   this.getSharedSecret_(onSharedSecretReceived);
    869 }
    870 
    871 /**
    872  * Gets shared secret to be used for connection.
    873  *
    874  * @param {function(string)} callback Callback called with the shared secret.
    875  * @return {void} Nothing.
    876  * @private
    877  */
    878 remoting.ClientSession.prototype.getSharedSecret_ = function(callback) {
    879   /** @type remoting.ClientSession */
    880   var that = this;
    881   if (this.plugin_.hasFeature(remoting.ClientPlugin.Feature.THIRD_PARTY_AUTH)) {
    882     /** @type{function(string, string, string): void} */
    883     var fetchThirdPartyToken = function(tokenUrl, hostPublicKey, scope) {
    884       that.fetchThirdPartyToken_(
    885           tokenUrl, hostPublicKey, scope,
    886           that.plugin_.onThirdPartyTokenFetched.bind(that.plugin_));
    887     };
    888     this.plugin_.setFetchThirdPartyTokenHandler(fetchThirdPartyToken);
    889   }
    890   if (this.accessCode_) {
    891     // Shared secret was already supplied before connecting (It2Me case).
    892     callback(this.accessCode_);
    893   } else if (this.plugin_.hasFeature(
    894       remoting.ClientPlugin.Feature.ASYNC_PIN)) {
    895     // Plugin supports asynchronously asking for the PIN.
    896     this.plugin_.useAsyncPinDialog();
    897     /** @param {boolean} pairingSupported */
    898     var fetchPin = function(pairingSupported) {
    899       that.fetchPin_(pairingSupported,
    900                      that.plugin_.onPinFetched.bind(that.plugin_));
    901     };
    902     this.plugin_.setFetchPinHandler(fetchPin);
    903     callback('');
    904   } else {
    905     // Clients that don't support asking for a PIN asynchronously also don't
    906     // support pairing, so request the PIN now without offering to remember it.
    907     this.fetchPin_(false, callback);
    908   }
    909 };
    910 
    911 /**
    912  * Callback that the plugin invokes to indicate that the connection
    913  * status has changed.
    914  *
    915  * @private
    916  * @param {number} status The plugin's status.
    917  * @param {number} error The plugin's error state, if any.
    918  */
    919 remoting.ClientSession.prototype.onConnectionStatusUpdate_ =
    920     function(status, error) {
    921   if (status == remoting.ClientSession.State.CONNECTED) {
    922     this.setFocusHandlers_();
    923     this.onDesktopSizeChanged_();
    924     if (this.resizeToClient_) {
    925       var clientArea = this.getClientArea_();
    926       this.plugin_.notifyClientResolution(clientArea.width,
    927                                           clientArea.height,
    928                                           window.devicePixelRatio);
    929     }
    930     // Activate full-screen related UX.
    931     remoting.fullscreen.addListener(this.callOnFullScreenChanged_);
    932     if (remoting.windowFrame) {
    933       remoting.windowFrame.setClientSession(this);
    934     } else {
    935       remoting.toolbar.setClientSession(this);
    936     }
    937     remoting.optionsMenu.setClientSession(this);
    938     document.body.classList.add('connected');
    939 
    940     this.container_.addEventListener('mousemove',
    941                                      this.updateMouseCursorPosition_,
    942                                      true);
    943 
    944   } else if (status == remoting.ClientSession.State.FAILED) {
    945     switch (error) {
    946       case remoting.ClientSession.ConnectionError.HOST_IS_OFFLINE:
    947         this.error_ = remoting.Error.HOST_IS_OFFLINE;
    948         break;
    949       case remoting.ClientSession.ConnectionError.SESSION_REJECTED:
    950         this.error_ = remoting.Error.INVALID_ACCESS_CODE;
    951         break;
    952       case remoting.ClientSession.ConnectionError.INCOMPATIBLE_PROTOCOL:
    953         this.error_ = remoting.Error.INCOMPATIBLE_PROTOCOL;
    954         break;
    955       case remoting.ClientSession.ConnectionError.NETWORK_FAILURE:
    956         this.error_ = remoting.Error.P2P_FAILURE;
    957         break;
    958       case remoting.ClientSession.ConnectionError.HOST_OVERLOAD:
    959         this.error_ = remoting.Error.HOST_OVERLOAD;
    960         break;
    961       default:
    962         this.error_ = remoting.Error.UNEXPECTED;
    963     }
    964   }
    965   this.setState_(/** @type {remoting.ClientSession.State} */ (status));
    966 };
    967 
    968 /**
    969  * Callback that the plugin invokes to indicate when the connection is
    970  * ready.
    971  *
    972  * @private
    973  * @param {boolean} ready True if the connection is ready.
    974  */
    975 remoting.ClientSession.prototype.onConnectionReady_ = function(ready) {
    976   if (!ready) {
    977     this.container_.classList.add('session-client-inactive');
    978   } else {
    979     this.container_.classList.remove('session-client-inactive');
    980   }
    981 
    982   this.raiseEvent(remoting.ClientSession.Events.videoChannelStateChanged,
    983                   ready);
    984 };
    985 
    986 /**
    987  * Called when the client-host capabilities negotiation is complete.
    988  *
    989  * @param {!Array.<string>} capabilities The set of capabilities negotiated
    990  *     between the client and host.
    991  * @return {void} Nothing.
    992  * @private
    993  */
    994 remoting.ClientSession.prototype.onSetCapabilities_ = function(capabilities) {
    995   if (this.capabilities_ != null) {
    996     console.error('onSetCapabilities_() is called more than once');
    997     return;
    998   }
    999 
   1000   this.capabilities_ = capabilities;
   1001   if (this.hasCapability_(
   1002       remoting.ClientSession.Capability.SEND_INITIAL_RESOLUTION)) {
   1003     var clientArea = this.getClientArea_();
   1004     this.plugin_.notifyClientResolution(clientArea.width,
   1005                                         clientArea.height,
   1006                                         window.devicePixelRatio);
   1007   }
   1008   if (this.hasCapability_(
   1009       remoting.ClientSession.Capability.VIDEO_RECORDER)) {
   1010     this.videoFrameRecorder_ = new remoting.VideoFrameRecorder(this.plugin_);
   1011   }
   1012 };
   1013 
   1014 /**
   1015  * @private
   1016  * @param {remoting.ClientSession.State} newState The new state for the session.
   1017  * @return {void} Nothing.
   1018  */
   1019 remoting.ClientSession.prototype.setState_ = function(newState) {
   1020   var oldState = this.state_;
   1021   this.state_ = newState;
   1022   var state = this.state_;
   1023   if (oldState == remoting.ClientSession.State.CONNECTING) {
   1024     if (this.state_ == remoting.ClientSession.State.CLOSED) {
   1025       state = remoting.ClientSession.State.CONNECTION_CANCELED;
   1026     } else if (this.state_ == remoting.ClientSession.State.FAILED &&
   1027         this.error_ == remoting.Error.HOST_IS_OFFLINE &&
   1028         !this.logHostOfflineErrors_) {
   1029       // The application requested host-offline errors to be suppressed, for
   1030       // example, because this connection attempt is using a cached host JID.
   1031       console.log('Suppressing host-offline error.');
   1032       state = remoting.ClientSession.State.CONNECTION_CANCELED;
   1033     }
   1034   } else if (oldState == remoting.ClientSession.State.CONNECTED &&
   1035              this.state_ == remoting.ClientSession.State.FAILED) {
   1036     state = remoting.ClientSession.State.CONNECTION_DROPPED;
   1037   }
   1038   this.logToServer.logClientSessionStateChange(state, this.error_, this.mode_);
   1039   if (this.state_ == remoting.ClientSession.State.CONNECTED) {
   1040     this.createGnubbyAuthHandler_();
   1041     this.createCastExtensionHandler_();
   1042   }
   1043 
   1044   this.raiseEvent(remoting.ClientSession.Events.stateChanged,
   1045     new remoting.ClientSession.StateEvent(newState, oldState)
   1046   );
   1047 };
   1048 
   1049 /**
   1050  * This is a callback that gets called when the window is resized.
   1051  *
   1052  * @return {void} Nothing.
   1053  */
   1054 remoting.ClientSession.prototype.onResize = function() {
   1055   this.updateDimensions();
   1056 
   1057   if (this.notifyClientResolutionTimer_) {
   1058     window.clearTimeout(this.notifyClientResolutionTimer_);
   1059     this.notifyClientResolutionTimer_ = null;
   1060   }
   1061 
   1062   // Defer notifying the host of the change until the window stops resizing, to
   1063   // avoid overloading the control channel with notifications.
   1064   if (this.resizeToClient_) {
   1065     var kResizeRateLimitMs = 1000;
   1066     if (this.hasCapability_(
   1067         remoting.ClientSession.Capability.RATE_LIMIT_RESIZE_REQUESTS)) {
   1068       kResizeRateLimitMs = 250;
   1069     }
   1070     var clientArea = this.getClientArea_();
   1071     this.notifyClientResolutionTimer_ = window.setTimeout(
   1072         this.plugin_.notifyClientResolution.bind(this.plugin_,
   1073                                                  clientArea.width,
   1074                                                  clientArea.height,
   1075                                                  window.devicePixelRatio),
   1076         kResizeRateLimitMs);
   1077   }
   1078 
   1079   // If bump-scrolling is enabled, adjust the plugin margins to fully utilize
   1080   // the new window area.
   1081   this.resetScroll_();
   1082 
   1083   this.updateScrollbarVisibility();
   1084 };
   1085 
   1086 /**
   1087  * Requests that the host pause or resume video updates.
   1088  *
   1089  * @param {boolean} pause True to pause video, false to resume.
   1090  * @return {void} Nothing.
   1091  */
   1092 remoting.ClientSession.prototype.pauseVideo = function(pause) {
   1093   if (this.plugin_) {
   1094     this.plugin_.pauseVideo(pause);
   1095   }
   1096 };
   1097 
   1098 /**
   1099  * Requests that the host pause or resume audio.
   1100  *
   1101  * @param {boolean} pause True to pause audio, false to resume.
   1102  * @return {void} Nothing.
   1103  */
   1104 remoting.ClientSession.prototype.pauseAudio = function(pause) {
   1105   if (this.plugin_) {
   1106     this.plugin_.pauseAudio(pause)
   1107   }
   1108 }
   1109 
   1110 /**
   1111  * This is a callback that gets called when the plugin notifies us of a change
   1112  * in the size of the remote desktop.
   1113  *
   1114  * @private
   1115  * @return {void} Nothing.
   1116  */
   1117 remoting.ClientSession.prototype.onDesktopSizeChanged_ = function() {
   1118   console.log('desktop size changed: ' +
   1119               this.plugin_.getDesktopWidth() + 'x' +
   1120               this.plugin_.getDesktopHeight() +' @ ' +
   1121               this.plugin_.getDesktopXDpi() + 'x' +
   1122               this.plugin_.getDesktopYDpi() + ' DPI');
   1123   this.updateDimensions();
   1124   this.updateScrollbarVisibility();
   1125 };
   1126 
   1127 /**
   1128  * Refreshes the plugin's dimensions, taking into account the sizes of the
   1129  * remote desktop and client window, and the current scale-to-fit setting.
   1130  *
   1131  * @return {void} Nothing.
   1132  */
   1133 remoting.ClientSession.prototype.updateDimensions = function() {
   1134   if (this.plugin_.getDesktopWidth() == 0 ||
   1135       this.plugin_.getDesktopHeight() == 0) {
   1136     return;
   1137   }
   1138 
   1139   var clientArea = this.getClientArea_();
   1140   var desktopWidth = this.plugin_.getDesktopWidth();
   1141   var desktopHeight = this.plugin_.getDesktopHeight();
   1142 
   1143   // When configured to display a host at its original size, we aim to display
   1144   // it as close to its physical size as possible, without losing data:
   1145   // - If client and host have matching DPI, render the host pixel-for-pixel.
   1146   // - If the host has higher DPI then still render pixel-for-pixel.
   1147   // - If the host has lower DPI then let Chrome up-scale it to natural size.
   1148 
   1149   // We specify the plugin dimensions in Density-Independent Pixels, so to
   1150   // render pixel-for-pixel we need to down-scale the host dimensions by the
   1151   // devicePixelRatio of the client. To match the host pixel density, we choose
   1152   // an initial scale factor based on the client devicePixelRatio and host DPI.
   1153 
   1154   // Determine the effective device pixel ratio of the host, based on DPI.
   1155   var hostPixelRatioX = Math.ceil(this.plugin_.getDesktopXDpi() / 96);
   1156   var hostPixelRatioY = Math.ceil(this.plugin_.getDesktopYDpi() / 96);
   1157   var hostPixelRatio = Math.min(hostPixelRatioX, hostPixelRatioY);
   1158 
   1159   // Down-scale by the smaller of the client and host ratios.
   1160   var scale = 1.0 / Math.min(window.devicePixelRatio, hostPixelRatio);
   1161 
   1162   if (this.shrinkToFit_) {
   1163     // Reduce the scale, if necessary, to fit the whole desktop in the window.
   1164     var scaleFitWidth = Math.min(scale, 1.0 * clientArea.width / desktopWidth);
   1165     var scaleFitHeight =
   1166         Math.min(scale, 1.0 * clientArea.height / desktopHeight);
   1167     scale = Math.min(scaleFitHeight, scaleFitWidth);
   1168 
   1169     // If we're running full-screen then try to handle common side-by-side
   1170     // multi-monitor combinations more intelligently.
   1171     if (remoting.fullscreen.isActive()) {
   1172       // If the host has two monitors each the same size as the client then
   1173       // scale-to-fit will have the desktop occupy only 50% of the client area,
   1174       // in which case it would be preferable to down-scale less and let the
   1175       // user bump-scroll around ("scale-and-pan").
   1176       // Triggering scale-and-pan if less than 65% of the client area would be
   1177       // used adds enough fuzz to cope with e.g. 1280x800 client connecting to
   1178       // a (2x1280)x1024 host nicely.
   1179       // Note that we don't need to account for scrollbars while fullscreen.
   1180       if (scale <= scaleFitHeight * 0.65) {
   1181         scale = scaleFitHeight;
   1182       }
   1183       if (scale <= scaleFitWidth * 0.65) {
   1184         scale = scaleFitWidth;
   1185       }
   1186     }
   1187   }
   1188 
   1189   var pluginWidth = Math.round(desktopWidth * scale);
   1190   var pluginHeight = Math.round(desktopHeight * scale);
   1191 
   1192   if (this.video_) {
   1193     this.video_.style.width = pluginWidth + 'px';
   1194     this.video_.style.height = pluginHeight + 'px';
   1195   }
   1196 
   1197   // Resize the plugin if necessary.
   1198   // TODO(wez): Handle high-DPI to high-DPI properly (crbug.com/135089).
   1199   this.plugin_.element().style.width = pluginWidth + 'px';
   1200   this.plugin_.element().style.height = pluginHeight + 'px';
   1201 
   1202   // Position the container.
   1203   // Note that clientWidth/Height take into account scrollbars.
   1204   var clientWidth = document.documentElement.clientWidth;
   1205   var clientHeight = document.documentElement.clientHeight;
   1206   var parentNode = this.plugin_.element().parentNode;
   1207 
   1208   console.log('plugin dimensions: ' +
   1209               parentNode.style.left + ',' +
   1210               parentNode.style.top + '-' +
   1211               pluginWidth + 'x' + pluginHeight + '.');
   1212 };
   1213 
   1214 /**
   1215  * Returns an associative array with a set of stats for this connection.
   1216  *
   1217  * @return {remoting.ClientSession.PerfStats} The connection statistics.
   1218  */
   1219 remoting.ClientSession.prototype.getPerfStats = function() {
   1220   return this.plugin_.getPerfStats();
   1221 };
   1222 
   1223 /**
   1224  * Logs statistics.
   1225  *
   1226  * @param {remoting.ClientSession.PerfStats} stats
   1227  */
   1228 remoting.ClientSession.prototype.logStatistics = function(stats) {
   1229   this.logToServer.logStatistics(stats, this.mode_);
   1230 };
   1231 
   1232 /**
   1233  * Enable or disable logging of connection errors due to a host being offline.
   1234  * For example, if attempting a connection using a cached JID, host-offline
   1235  * errors should not be logged because the JID will be refreshed and the
   1236  * connection retried.
   1237  *
   1238  * @param {boolean} enable True to log host-offline errors; false to suppress.
   1239  */
   1240 remoting.ClientSession.prototype.logHostOfflineErrors = function(enable) {
   1241   this.logHostOfflineErrors_ = enable;
   1242 };
   1243 
   1244 /**
   1245  * Request pairing with the host for PIN-less authentication.
   1246  *
   1247  * @param {string} clientName The human-readable name of the client.
   1248  * @param {function(string, string):void} onDone Callback to receive the
   1249  *     client id and shared secret when they are available.
   1250  */
   1251 remoting.ClientSession.prototype.requestPairing = function(clientName, onDone) {
   1252   if (this.plugin_) {
   1253     this.plugin_.requestPairing(clientName, onDone);
   1254   }
   1255 };
   1256 
   1257 /**
   1258  * Called when the full-screen status has changed, either via the
   1259  * remoting.Fullscreen class, or via a system event such as the Escape key
   1260  *
   1261  * @param {boolean} fullscreen True if the app is entering full-screen mode;
   1262  *     false if it is leaving it.
   1263  * @private
   1264  */
   1265 remoting.ClientSession.prototype.onFullScreenChanged_ = function (fullscreen) {
   1266   var htmlNode = /** @type {HTMLElement} */ (document.documentElement);
   1267   this.enableBumpScroll_(fullscreen);
   1268   if (fullscreen) {
   1269     htmlNode.classList.add('full-screen');
   1270   } else {
   1271     htmlNode.classList.remove('full-screen');
   1272   }
   1273 };
   1274 
   1275 /**
   1276  * Scroll the client plugin by the specified amount, keeping it visible.
   1277  * Note that this is only used in content full-screen mode (not windowed or
   1278  * browser full-screen modes), where window.scrollBy and the scrollTop and
   1279  * scrollLeft properties don't work.
   1280  * @param {number} dx The amount by which to scroll horizontally. Positive to
   1281  *     scroll right; negative to scroll left.
   1282  * @param {number} dy The amount by which to scroll vertically. Positive to
   1283  *     scroll down; negative to scroll up.
   1284  * @return {boolean} True if the requested scroll had no effect because both
   1285  *     vertical and horizontal edges of the screen have been reached.
   1286  * @private
   1287  */
   1288 remoting.ClientSession.prototype.scroll_ = function(dx, dy) {
   1289   /**
   1290    * Helper function for x- and y-scrolling
   1291    * @param {number|string} curr The current margin, eg. "10px".
   1292    * @param {number} delta The requested scroll amount.
   1293    * @param {number} windowBound The size of the window, in pixels.
   1294    * @param {number} pluginBound The size of the plugin, in pixels.
   1295    * @param {{stop: boolean}} stop Reference parameter used to indicate when
   1296    *     the scroll has reached one of the edges and can be stopped in that
   1297    *     direction.
   1298    * @return {string} The new margin value.
   1299    */
   1300   var adjustMargin = function(curr, delta, windowBound, pluginBound, stop) {
   1301     var minMargin = Math.min(0, windowBound - pluginBound);
   1302     var result = (curr ? parseFloat(curr) : 0) - delta;
   1303     result = Math.min(0, Math.max(minMargin, result));
   1304     stop.stop = (result == 0 || result == minMargin);
   1305     return result + 'px';
   1306   };
   1307 
   1308   var plugin = this.plugin_.element();
   1309   var style = this.container_.style;
   1310 
   1311   var stopX = { stop: false };
   1312   var clientArea = this.getClientArea_();
   1313   style.marginLeft = adjustMargin(style.marginLeft, dx, clientArea.width,
   1314       this.pluginWidthForBumpScrollTesting || plugin.clientWidth, stopX);
   1315 
   1316   var stopY = { stop: false };
   1317   style.marginTop = adjustMargin(
   1318       style.marginTop, dy, clientArea.height,
   1319       this.pluginHeightForBumpScrollTesting || plugin.clientHeight, stopY);
   1320   return stopX.stop && stopY.stop;
   1321 };
   1322 
   1323 remoting.ClientSession.prototype.resetScroll_ = function() {
   1324   this.container_.style.marginTop = '0px';
   1325   this.container_.style.marginLeft = '0px';
   1326 };
   1327 
   1328 /**
   1329  * Enable or disable bump-scrolling. When disabling bump scrolling, also reset
   1330  * the scroll offsets to (0, 0).
   1331  * @private
   1332  * @param {boolean} enable True to enable bump-scrolling, false to disable it.
   1333  */
   1334 remoting.ClientSession.prototype.enableBumpScroll_ = function(enable) {
   1335   var element = /*@type{HTMLElement} */ document.documentElement;
   1336   if (enable) {
   1337     /** @type {null|function(Event):void} */
   1338     this.onMouseMoveRef_ = this.onMouseMove_.bind(this);
   1339     element.addEventListener('mousemove', this.onMouseMoveRef_, false);
   1340   } else {
   1341     element.removeEventListener('mousemove', this.onMouseMoveRef_, false);
   1342     this.onMouseMoveRef_ = null;
   1343     this.resetScroll_();
   1344   }
   1345 };
   1346 
   1347 /**
   1348  * @param {Event} event The mouse event.
   1349  * @private
   1350  */
   1351 remoting.ClientSession.prototype.onMouseMove_ = function(event) {
   1352   if (this.bumpScrollTimer_) {
   1353     window.clearTimeout(this.bumpScrollTimer_);
   1354     this.bumpScrollTimer_ = null;
   1355   }
   1356 
   1357   /**
   1358    * Compute the scroll speed based on how close the mouse is to the edge.
   1359    * @param {number} mousePos The mouse x- or y-coordinate
   1360    * @param {number} size The width or height of the content area.
   1361    * @return {number} The scroll delta, in pixels.
   1362    */
   1363   var computeDelta = function(mousePos, size) {
   1364     var threshold = 10;
   1365     if (mousePos >= size - threshold) {
   1366       return 1 + 5 * (mousePos - (size - threshold)) / threshold;
   1367     } else if (mousePos <= threshold) {
   1368       return -1 - 5 * (threshold - mousePos) / threshold;
   1369     }
   1370     return 0;
   1371   };
   1372 
   1373   var clientArea = this.getClientArea_();
   1374   var dx = computeDelta(event.x, clientArea.width);
   1375   var dy = computeDelta(event.y, clientArea.height);
   1376 
   1377   if (dx != 0 || dy != 0) {
   1378     this.raiseEvent(remoting.ClientSession.Events.bumpScrollStarted);
   1379     /** @type {remoting.ClientSession} */
   1380     var that = this;
   1381     /**
   1382      * Scroll the view, and schedule a timer to do so again unless we've hit
   1383      * the edges of the screen. This timer is cancelled when the mouse moves.
   1384      * @param {number} expected The time at which we expect to be called.
   1385      */
   1386     var repeatScroll = function(expected) {
   1387       /** @type {number} */
   1388       var now = new Date().getTime();
   1389       /** @type {number} */
   1390       var timeout = 10;
   1391       var lateAdjustment = 1 + (now - expected) / timeout;
   1392       if (that.scroll_(lateAdjustment * dx, lateAdjustment * dy)) {
   1393         that.raiseEvent(remoting.ClientSession.Events.bumpScrollStopped);
   1394       } else {
   1395         that.bumpScrollTimer_ = window.setTimeout(
   1396             function() { repeatScroll(now + timeout); },
   1397             timeout);
   1398       }
   1399     };
   1400     repeatScroll(new Date().getTime());
   1401   }
   1402 };
   1403 
   1404 /**
   1405  * Sends a clipboard item to the host.
   1406  *
   1407  * @param {string} mimeType The MIME type of the clipboard item.
   1408  * @param {string} item The clipboard item.
   1409  */
   1410 remoting.ClientSession.prototype.sendClipboardItem = function(mimeType, item) {
   1411   if (!this.plugin_)
   1412     return;
   1413   this.plugin_.sendClipboardItem(mimeType, item);
   1414 };
   1415 
   1416 /**
   1417  * Send a gnubby-auth extension message to the host.
   1418  * @param {Object} data The gnubby-auth message data.
   1419  */
   1420 remoting.ClientSession.prototype.sendGnubbyAuthMessage = function(data) {
   1421   if (!this.plugin_)
   1422     return;
   1423   this.plugin_.sendClientMessage('gnubby-auth', JSON.stringify(data));
   1424 };
   1425 
   1426 /**
   1427  * Process a remote gnubby auth request.
   1428  * @param {string} data Remote gnubby request data.
   1429  * @private
   1430  */
   1431 remoting.ClientSession.prototype.processGnubbyAuthMessage_ = function(data) {
   1432   if (this.gnubbyAuthHandler_) {
   1433     try {
   1434       this.gnubbyAuthHandler_.onMessage(data);
   1435     } catch (err) {
   1436       console.error('Failed to process gnubby message: ',
   1437           /** @type {*} */ (err));
   1438     }
   1439   } else {
   1440     console.error('Received unexpected gnubby message');
   1441   }
   1442 };
   1443 
   1444 /**
   1445  * Create a gnubby auth handler and inform the host that gnubby auth is
   1446  * supported.
   1447  * @private
   1448  */
   1449 remoting.ClientSession.prototype.createGnubbyAuthHandler_ = function() {
   1450   if (this.mode_ == remoting.ClientSession.Mode.ME2ME) {
   1451     this.gnubbyAuthHandler_ = new remoting.GnubbyAuthHandler(this);
   1452     // TODO(psj): Move to more generic capabilities mechanism.
   1453     this.sendGnubbyAuthMessage({'type': 'control', 'option': 'auth-v1'});
   1454   }
   1455 };
   1456 
   1457 /**
   1458  * @return {{width: number, height: number}} The height of the window's client
   1459  *     area. This differs between apps v1 and apps v2 due to the custom window
   1460  *     borders used by the latter.
   1461  * @private
   1462  */
   1463 remoting.ClientSession.prototype.getClientArea_ = function() {
   1464   return remoting.windowFrame ?
   1465       remoting.windowFrame.getClientArea() :
   1466       { 'width': window.innerWidth, 'height': window.innerHeight };
   1467 };
   1468 
   1469 /**
   1470  * @param {string} url
   1471  * @param {number} hotspotX
   1472  * @param {number} hotspotY
   1473  */
   1474 remoting.ClientSession.prototype.updateMouseCursorImage_ =
   1475     function(url, hotspotX, hotspotY) {
   1476   this.mouseCursorOverlay_.hidden = !url;
   1477   if (url) {
   1478     this.mouseCursorOverlay_.style.marginLeft = '-' + hotspotX + 'px';
   1479     this.mouseCursorOverlay_.style.marginTop = '-' + hotspotY + 'px';
   1480     this.mouseCursorOverlay_.src = url;
   1481   }
   1482 };
   1483 
   1484 /**
   1485  * @return {{top: number, left:number}} The top-left corner of the plugin.
   1486  */
   1487 remoting.ClientSession.prototype.getPluginPositionForTesting = function() {
   1488   var style = this.container_.style;
   1489   return {
   1490     top: parseFloat(style.marginTop),
   1491     left: parseFloat(style.marginLeft)
   1492   };
   1493 };
   1494 
   1495 /**
   1496  * Send a Cast extension message to the host.
   1497  * @param {Object} data The cast message data.
   1498  */
   1499 remoting.ClientSession.prototype.sendCastExtensionMessage = function(data) {
   1500   if (!this.plugin_)
   1501     return;
   1502   this.plugin_.sendClientMessage('cast_message', JSON.stringify(data));
   1503 };
   1504 
   1505 /**
   1506  * Process a remote Cast extension message from the host.
   1507  * @param {string} data Remote cast extension data message.
   1508  * @private
   1509  */
   1510 remoting.ClientSession.prototype.processCastExtensionMessage_ = function(data) {
   1511   if (this.castExtensionHandler_) {
   1512     try {
   1513       this.castExtensionHandler_.onMessage(data);
   1514     } catch (err) {
   1515       console.error('Failed to process cast message: ',
   1516           /** @type {*} */ (err));
   1517     }
   1518   } else {
   1519     console.error('Received unexpected cast message');
   1520   }
   1521 };
   1522 
   1523 /**
   1524  * Create a CastExtensionHandler and inform the host that cast extension
   1525  * is supported.
   1526  * @private
   1527  */
   1528 remoting.ClientSession.prototype.createCastExtensionHandler_ = function() {
   1529   if (remoting.enableCast && this.mode_ == remoting.ClientSession.Mode.ME2ME) {
   1530     this.castExtensionHandler_ = new remoting.CastExtensionHandler(this);
   1531   }
   1532 };
   1533 
   1534 /**
   1535  * Returns true if the ClientSession can record video frames to a file.
   1536  * @return {boolean}
   1537  */
   1538 remoting.ClientSession.prototype.canRecordVideo = function() {
   1539   return !!this.videoFrameRecorder_;
   1540 }
   1541 
   1542 /**
   1543  * Returns true if the ClientSession is currently recording video frames.
   1544  * @return {boolean}
   1545  */
   1546 remoting.ClientSession.prototype.isRecordingVideo = function() {
   1547   if (!this.videoFrameRecorder_) {
   1548     return false;
   1549   }
   1550   return this.videoFrameRecorder_.isRecording();
   1551 }
   1552 
   1553 /**
   1554  * Starts or stops recording of video frames.
   1555  */
   1556 remoting.ClientSession.prototype.startStopRecording = function() {
   1557   if (this.videoFrameRecorder_) {
   1558     this.videoFrameRecorder_.startStopRecording();
   1559   }
   1560 }
   1561 
   1562 /**
   1563  * Handles protocol extension messages.
   1564  * @param {string} type Type of extension message.
   1565  * @param {string} data Contents of the extension message.
   1566  * @return {boolean} True if the message was recognized, false otherwise.
   1567  */
   1568 remoting.ClientSession.prototype.handleExtensionMessage =
   1569     function(type, data) {
   1570   if (this.videoFrameRecorder_) {
   1571     return this.videoFrameRecorder_.handleMessage(type, data);
   1572   }
   1573   return false;
   1574 }
   1575