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