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.
7 * Class handling creation and teardown of a remoting client session.
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,
15 * This class should not access the plugin directly, instead it should
16 * do it through ClientPlugin class which abstracts plugin version
22 /** @suppress {duplicate} */
23 var remoting = remoting || {};
26 * @param {HTMLElement} container Container element for the client view.
27 * @param {string} hostDisplayName A human-readable name for the host.
28 * @param {string} accessCode The IT2Me access code. Blank for Me2Me.
29 * @param {function(boolean, function(string): void): void} fetchPin
30 * Called by Me2Me connections when a PIN needs to be obtained
32 * @param {function(string, string, string,
33 * function(string, string): void): void}
34 * fetchThirdPartyToken Called by Me2Me connections when a third party
35 * authentication token must be obtained.
36 * @param {string} authenticationMethods Comma-separated list of
37 * authentication methods the client should attempt to use.
38 * @param {string} hostId The host identifier for Me2Me, or empty for IT2Me.
39 * Mixed into authentication hashes for some authentication methods.
40 * @param {string} hostJid The jid of the host to connect to.
41 * @param {string} hostPublicKey The base64 encoded version of the host's
43 * @param {remoting.ClientSession.Mode} mode The mode of this connection.
44 * @param {string} clientPairingId For paired Me2Me connections, the
45 * pairing id for this client, as issued by the host.
46 * @param {string} clientPairedSecret For paired Me2Me connections, the
47 * paired secret for this client, as issued by the host.
49 * @extends {base.EventSource}
51 remoting.ClientSession = function(container, hostDisplayName, accessCode,
52 fetchPin, fetchThirdPartyToken,
53 authenticationMethods, hostId, hostJid,
54 hostPublicKey, mode, clientPairingId,
57 this.state_ = remoting.ClientSession.State.CREATED;
60 this.error_ = remoting.Error.NONE;
62 /** @type {HTMLElement}
64 this.container_ = container;
67 this.hostDisplayName_ = hostDisplayName;
69 this.hostJid_ = hostJid;
71 this.hostPublicKey_ = hostPublicKey;
73 this.accessCode_ = accessCode;
75 this.fetchPin_ = fetchPin;
77 this.fetchThirdPartyToken_ = fetchThirdPartyToken;
79 this.authenticationMethods_ = authenticationMethods;
81 this.hostId_ = hostId;
85 this.clientPairingId_ = clientPairingId;
87 this.clientPairedSecret_ = clientPairedSecret;
90 /** @type {remoting.ClientPlugin}
94 this.shrinkToFit_ = true;
96 this.resizeToClient_ = true;
100 this.hasReceivedFrame_ = false;
101 this.logToServer = new remoting.LogToServer();
103 /** @type {number?} @private */
104 this.notifyClientResolutionTimer_ = null;
105 /** @type {number?} @private */
106 this.bumpScrollTimer_ = null;
108 // Bump-scroll test variables. Override to use a fake value for the width
109 // and height of the client plugin so that bump-scrolling can be tested
110 // without relying on the actual size of the host desktop.
111 /** @type {number} @private */
112 this.pluginWidthForBumpScrollTesting = 0;
113 /** @type {number} @private */
114 this.pluginHeightForBumpScrollTesting = 0;
117 * Allow host-offline error reporting to be suppressed in situations where it
118 * would not be useful, for example, when using a cached host JID.
120 * @type {boolean} @private
122 this.logHostOfflineErrors_ = true;
125 this.callPluginLostFocus_ = this.pluginLostFocus_.bind(this);
127 this.callPluginGotFocus_ = this.pluginGotFocus_.bind(this);
129 this.callToggleFullScreen_ = remoting.fullscreen.toggle.bind(
130 remoting.fullscreen);
132 this.callOnFullScreenChanged_ = this.onFullScreenChanged_.bind(this)
135 this.screenOptionsMenu_ = new remoting.MenuButton(
136 document.getElementById('screen-options-menu'),
137 this.onShowOptionsMenu_.bind(this));
139 this.sendKeysMenu_ = new remoting.MenuButton(
140 document.getElementById('send-keys-menu')
143 /** @type {HTMLMediaElement} @private */
146 /** @type {Element} @private */
147 this.mouseCursorOverlay_ =
148 this.container_.querySelector('.mouse-cursor-overlay');
150 /** @type {Element} */
151 var img = this.mouseCursorOverlay_;
152 /** @param {Event} event @private */
153 this.updateMouseCursorPosition_ = function(event) {
154 img.style.top = event.y + 'px';
155 img.style.left = event.x + 'px';
158 /** @type {HTMLElement} @private */
159 this.resizeToClientButton_ =
160 document.getElementById('screen-resize-to-client');
161 /** @type {HTMLElement} @private */
162 this.shrinkToFitButton_ = document.getElementById('screen-shrink-to-fit');
163 /** @type {HTMLElement} @private */
164 this.fullScreenButton_ = document.getElementById('toggle-full-screen');
166 /** @type {remoting.GnubbyAuthHandler} @private */
167 this.gnubbyAuthHandler_ = null;
169 if (this.mode_ == remoting.ClientSession.Mode.IT2ME) {
170 // Resize-to-client is not supported for IT2Me hosts.
171 this.resizeToClientButton_.hidden = true;
173 this.resizeToClientButton_.hidden = false;
176 this.fullScreenButton_.addEventListener(
177 'click', this.callToggleFullScreen_, false);
178 this.defineEvents(Object.keys(remoting.ClientSession.Events));
181 base.extend(remoting.ClientSession, base.EventSource);
183 /** @enum {string} */
184 remoting.ClientSession.Events = {
185 stateChanged: 'stateChanged',
186 videoChannelStateChanged: 'videoChannelStateChanged',
187 bumpScrollStarted: 'bumpScrollStarted',
188 bumpScrollStopped: 'bumpScrollStopped'
192 * Get host display name.
196 remoting.ClientSession.prototype.getHostDisplayName = function() {
197 return this.hostDisplayName_;
201 * Called when the window or desktop size or the scaling settings change,
202 * to set the scroll-bar visibility.
204 * TODO(jamiewalch): crbug.com/252796: Remove this once crbug.com/240772 is
207 remoting.ClientSession.prototype.updateScrollbarVisibility = function() {
208 var needsVerticalScroll = false;
209 var needsHorizontalScroll = false;
210 if (!this.shrinkToFit_) {
211 // Determine whether or not horizontal or vertical scrollbars are
212 // required, taking into account their width.
213 var clientArea = this.getClientArea_();
214 needsVerticalScroll = clientArea.height < this.plugin_.desktopHeight;
215 needsHorizontalScroll = clientArea.width < this.plugin_.desktopWidth;
216 var kScrollBarWidth = 16;
217 if (needsHorizontalScroll && !needsVerticalScroll) {
218 needsVerticalScroll =
219 clientArea.height - kScrollBarWidth < this.plugin_.desktopHeight;
220 } else if (!needsHorizontalScroll && needsVerticalScroll) {
221 needsHorizontalScroll =
222 clientArea.width - kScrollBarWidth < this.plugin_.desktopWidth;
226 var scroller = document.getElementById('scroller');
227 if (needsHorizontalScroll) {
228 scroller.classList.remove('no-horizontal-scroll');
230 scroller.classList.add('no-horizontal-scroll');
232 if (needsVerticalScroll) {
233 scroller.classList.remove('no-vertical-scroll');
235 scroller.classList.add('no-vertical-scroll');
240 * @return {boolean} True if shrink-to-fit is enabled; false otherwise.
242 remoting.ClientSession.prototype.getShrinkToFit = function() {
243 return this.shrinkToFit_;
247 * @return {boolean} True if resize-to-client is enabled; false otherwise.
249 remoting.ClientSession.prototype.getResizeToClient = function() {
250 return this.resizeToClient_;
253 // Note that the positive values in both of these enums are copied directly
254 // from chromoting_scriptable_object.h and must be kept in sync. The negative
255 // values represent state transitions that occur within the web-app that have
256 // no corresponding plugin state transition.
257 /** @enum {number} */
258 remoting.ClientSession.State = {
259 CONNECTION_CANCELED: -3, // Connection closed (gracefully) before connecting.
260 CONNECTION_DROPPED: -2, // Succeeded, but subsequently closed with an error.
271 * @param {string} state The state name.
272 * @return {remoting.ClientSession.State} The session state enum value.
274 remoting.ClientSession.State.fromString = function(state) {
275 if (!remoting.ClientSession.State.hasOwnProperty(state)) {
276 throw "Invalid ClientSession.State: " + state;
278 return remoting.ClientSession.State[state];
283 @param {remoting.ClientSession.State} current
284 @param {remoting.ClientSession.State} previous
286 remoting.ClientSession.StateEvent = function(current, previous) {
287 /** @type {remoting.ClientSession.State} */
288 this.previous = previous
290 /** @type {remoting.ClientSession.State} */
291 this.current = current;
294 /** @enum {number} */
295 remoting.ClientSession.ConnectionError = {
300 INCOMPATIBLE_PROTOCOL: 3,
306 * @param {string} error The connection error name.
307 * @return {remoting.ClientSession.ConnectionError} The connection error enum.
309 remoting.ClientSession.ConnectionError.fromString = function(error) {
310 if (!remoting.ClientSession.ConnectionError.hasOwnProperty(error)) {
311 console.error('Unexpected ClientSession.ConnectionError string: ', error);
312 return remoting.ClientSession.ConnectionError.UNKNOWN;
314 return remoting.ClientSession.ConnectionError[error];
317 // The mode of this session.
318 /** @enum {number} */
319 remoting.ClientSession.Mode = {
325 * Type used for performance statistics collected by the plugin.
328 remoting.ClientSession.PerfStats = function() {};
329 /** @type {number} */
330 remoting.ClientSession.PerfStats.prototype.videoBandwidth;
331 /** @type {number} */
332 remoting.ClientSession.PerfStats.prototype.videoFrameRate;
333 /** @type {number} */
334 remoting.ClientSession.PerfStats.prototype.captureLatency;
335 /** @type {number} */
336 remoting.ClientSession.PerfStats.prototype.encodeLatency;
337 /** @type {number} */
338 remoting.ClientSession.PerfStats.prototype.decodeLatency;
339 /** @type {number} */
340 remoting.ClientSession.PerfStats.prototype.renderLatency;
341 /** @type {number} */
342 remoting.ClientSession.PerfStats.prototype.roundtripLatency;
344 // Keys for connection statistics.
345 remoting.ClientSession.STATS_KEY_VIDEO_BANDWIDTH = 'videoBandwidth';
346 remoting.ClientSession.STATS_KEY_VIDEO_FRAME_RATE = 'videoFrameRate';
347 remoting.ClientSession.STATS_KEY_CAPTURE_LATENCY = 'captureLatency';
348 remoting.ClientSession.STATS_KEY_ENCODE_LATENCY = 'encodeLatency';
349 remoting.ClientSession.STATS_KEY_DECODE_LATENCY = 'decodeLatency';
350 remoting.ClientSession.STATS_KEY_RENDER_LATENCY = 'renderLatency';
351 remoting.ClientSession.STATS_KEY_ROUNDTRIP_LATENCY = 'roundtripLatency';
353 // Keys for per-host settings.
354 remoting.ClientSession.KEY_REMAP_KEYS = 'remapKeys';
355 remoting.ClientSession.KEY_RESIZE_TO_CLIENT = 'resizeToClient';
356 remoting.ClientSession.KEY_SHRINK_TO_FIT = 'shrinkToFit';
359 * Set of capabilities for which hasCapability_() can be used to test.
363 remoting.ClientSession.Capability = {
364 // When enabled this capability causes the client to send its screen
365 // resolution to the host once connection has been established. See
366 // this.plugin_.notifyClientResolution().
367 SEND_INITIAL_RESOLUTION: 'sendInitialResolution',
368 RATE_LIMIT_RESIZE_REQUESTS: 'rateLimitResizeRequests',
369 VIDEO_RECORDER: 'videoRecorder'
373 * The set of capabilities negotiated between the client and host.
374 * @type {Array.<string>}
377 remoting.ClientSession.prototype.capabilities_ = null;
380 * @param {remoting.ClientSession.Capability} capability The capability to test
382 * @return {boolean} True if the capability has been negotiated between
383 * the client and host.
386 remoting.ClientSession.prototype.hasCapability_ = function(capability) {
387 if (this.capabilities_ == null)
390 return this.capabilities_.indexOf(capability) > -1;
394 * Callback function called when the plugin element gets focus.
396 remoting.ClientSession.prototype.pluginGotFocus_ = function() {
397 remoting.clipboard.initiateToHost();
401 * Callback function called when the plugin element loses focus.
403 remoting.ClientSession.prototype.pluginLostFocus_ = function() {
405 // Release all keys to prevent them becoming 'stuck down' on the host.
406 this.plugin_.releaseAllKeys();
407 if (this.plugin_.element()) {
408 // Focus should stay on the element, not (for example) the toolbar.
409 // Due to crbug.com/246335, we can't restore the focus immediately,
410 // otherwise the plugin gets confused about whether or not it has focus.
412 this.plugin_.element().focus.bind(this.plugin_.element()), 0);
418 * Adds <embed> element to |container| and readies the sesion object.
420 * @param {function(string, string):boolean} onExtensionMessage The handler for
421 * protocol extension messages. Returns true if a message is recognized;
424 remoting.ClientSession.prototype.createPluginAndConnect =
425 function(onExtensionMessage) {
426 this.plugin_ = new remoting.ClientPlugin(
427 this.container_.querySelector('.client-plugin-container'),
429 remoting.HostSettings.load(this.hostId_,
430 this.onHostSettingsLoaded_.bind(this));
434 * @param {Object.<string>} options The current options for the host, or {}
435 * if this client has no saved settings for the host.
438 remoting.ClientSession.prototype.onHostSettingsLoaded_ = function(options) {
439 if (remoting.ClientSession.KEY_REMAP_KEYS in options &&
440 typeof(options[remoting.ClientSession.KEY_REMAP_KEYS]) ==
442 this.remapKeys_ = /** @type {string} */
443 options[remoting.ClientSession.KEY_REMAP_KEYS];
445 if (remoting.ClientSession.KEY_RESIZE_TO_CLIENT in options &&
446 typeof(options[remoting.ClientSession.KEY_RESIZE_TO_CLIENT]) ==
448 this.resizeToClient_ = /** @type {boolean} */
449 options[remoting.ClientSession.KEY_RESIZE_TO_CLIENT];
451 if (remoting.ClientSession.KEY_SHRINK_TO_FIT in options &&
452 typeof(options[remoting.ClientSession.KEY_SHRINK_TO_FIT]) ==
454 this.shrinkToFit_ = /** @type {boolean} */
455 options[remoting.ClientSession.KEY_SHRINK_TO_FIT];
458 /** @param {boolean} result */
459 this.plugin_.initialize(this.onPluginInitialized_.bind(this));
463 * Constrains the focus to the plugin element.
466 remoting.ClientSession.prototype.setFocusHandlers_ = function() {
467 this.plugin_.element().addEventListener(
468 'focus', this.callPluginGotFocus_, false);
469 this.plugin_.element().addEventListener(
470 'blur', this.callPluginLostFocus_, false);
471 this.plugin_.element().focus();
475 * @param {remoting.Error} error
477 remoting.ClientSession.prototype.resetWithError_ = function(error) {
478 this.plugin_.cleanup();
481 this.setState_(remoting.ClientSession.State.FAILED);
485 * @param {boolean} initialized
487 remoting.ClientSession.prototype.onPluginInitialized_ = function(initialized) {
489 console.error('ERROR: remoting plugin not loaded');
490 this.resetWithError_(remoting.Error.MISSING_PLUGIN);
494 if (!this.plugin_.isSupportedVersion()) {
495 this.resetWithError_(remoting.Error.BAD_PLUGIN_VERSION);
499 // Show the Send Keys menu only if the plugin has the injectKeyEvent feature,
500 // and the Ctrl-Alt-Del button only in Me2Me mode.
501 if (!this.plugin_.hasFeature(
502 remoting.ClientPlugin.Feature.INJECT_KEY_EVENT)) {
503 var sendKeysElement = document.getElementById('send-keys-menu');
504 sendKeysElement.hidden = true;
505 } else if (this.mode_ != remoting.ClientSession.Mode.ME2ME) {
506 var sendCadElement = document.getElementById('send-ctrl-alt-del');
507 sendCadElement.hidden = true;
510 // Apply customized key remappings if the plugin supports remapKeys.
511 if (this.plugin_.hasFeature(remoting.ClientPlugin.Feature.REMAP_KEY)) {
512 this.applyRemapKeys_(true);
516 // Enable MediaSource-based rendering on Chrome 37 and above.
517 var chromeVersionMajor =
518 parseInt((remoting.getChromeVersion() || '0').split('.')[0], 10);
519 if (chromeVersionMajor >= 37 &&
520 this.plugin_.hasFeature(
521 remoting.ClientPlugin.Feature.MEDIA_SOURCE_RENDERING)) {
522 this.video_ = /** @type {HTMLMediaElement} */(
523 this.container_.querySelector('video'));
524 // Make sure that the <video> element is hidden until we get the first
526 this.video_.style.width = '0px';
527 this.video_.style.height = '0px';
529 var renderer = new remoting.MediaSourceRenderer(this.video_);
530 this.plugin_.enableMediaSourceRendering(renderer);
531 this.container_.classList.add('mediasource-rendering');
533 this.container_.classList.remove('mediasource-rendering');
536 /** @param {string} msg The IQ stanza to send. */
537 this.plugin_.onOutgoingIqHandler = this.sendIq_.bind(this);
538 /** @param {string} msg The message to log. */
539 this.plugin_.onDebugMessageHandler = function(msg) {
540 console.log('plugin: ' + msg.trimRight());
543 this.plugin_.onConnectionStatusUpdateHandler =
544 this.onConnectionStatusUpdate_.bind(this);
545 this.plugin_.onConnectionReadyHandler = this.onConnectionReady_.bind(this);
546 this.plugin_.onDesktopSizeUpdateHandler =
547 this.onDesktopSizeChanged_.bind(this);
548 this.plugin_.onSetCapabilitiesHandler = this.onSetCapabilities_.bind(this);
549 this.plugin_.onGnubbyAuthHandler = this.processGnubbyAuthMessage_.bind(this);
550 this.plugin_.updateMouseCursorImage = this.updateMouseCursorImage_.bind(this);
551 this.initiateConnection_();
555 * Deletes the <embed> element from the container, without sending a
556 * session_terminate request. This is to be called when the session was
557 * disconnected by the Host.
559 * @return {void} Nothing.
561 remoting.ClientSession.prototype.removePlugin = function() {
563 this.plugin_.element().removeEventListener(
564 'focus', this.callPluginGotFocus_, false);
565 this.plugin_.element().removeEventListener(
566 'blur', this.callPluginLostFocus_, false);
567 this.plugin_.cleanup();
571 // Delete event handlers that aren't relevent when not connected.
572 this.fullScreenButton_.removeEventListener(
573 'click', this.callToggleFullScreen_, false);
575 // Leave full-screen mode, and stop listening for related events.
576 var listener = this.callOnFullScreenChanged_;
577 remoting.fullscreen.syncWithMaximize(false);
578 remoting.fullscreen.activate(
581 remoting.fullscreen.removeListener(listener);
583 if (remoting.windowFrame) {
584 remoting.windowFrame.setClientSession(null);
586 remoting.toolbar.setClientSession(null);
589 // Remove mediasource-rendering class from the container - this will also
590 // hide the <video> element.
591 this.container_.classList.remove('mediasource-rendering');
593 this.container_.removeEventListener('mousemove',
594 this.updateMouseCursorPosition_,
599 * Disconnect the current session with a particular |error|. The session will
600 * raise a |stateChanged| event in response to it. The caller should then call
601 * |cleanup| to remove and destroy the <embed> element.
603 * @param {remoting.Error} error The reason for the disconnection. Use
604 * remoting.Error.NONE if there is no error.
605 * @return {void} Nothing.
607 remoting.ClientSession.prototype.disconnect = function(error) {
608 var state = (error == remoting.Error.NONE) ?
609 remoting.ClientSession.State.CLOSED :
610 remoting.ClientSession.State.FAILED;
612 // The plugin won't send a state change notification, so we explicitly log
613 // the fact that the connection has closed.
614 this.logToServer.logClientSessionStateChange(state, error, this.mode_);
616 this.setState_(state);
620 * Deletes the <embed> element from the container and disconnects.
622 * @return {void} Nothing.
624 remoting.ClientSession.prototype.cleanup = function() {
625 remoting.wcsSandbox.setOnIq(null);
628 'to="' + this.hostJid_ + '" ' +
630 'id="session-terminate" ' +
631 'xmlns:cli="jabber:client">' +
633 'xmlns="urn:xmpp:jingle:1" ' +
634 'action="session-terminate" ' +
635 'sid="' + this.sessionId_ + '">' +
636 '<reason><success/></reason>' +
643 * @return {remoting.ClientSession.Mode} The current state.
645 remoting.ClientSession.prototype.getMode = function() {
650 * @return {remoting.ClientSession.State} The current state.
652 remoting.ClientSession.prototype.getState = function() {
657 * @return {remoting.Error} The current error code.
659 remoting.ClientSession.prototype.getError = function() {
664 * Sends a key combination to the remoting client, by sending down events for
665 * the given keys, followed by up events in reverse order.
668 * @param {[number]} keys Key codes to be sent.
669 * @return {void} Nothing.
671 remoting.ClientSession.prototype.sendKeyCombination_ = function(keys) {
672 for (var i = 0; i < keys.length; i++) {
673 this.plugin_.injectKeyEvent(keys[i], true);
675 for (var i = 0; i < keys.length; i++) {
676 this.plugin_.injectKeyEvent(keys[i], false);
681 * Sends a Ctrl-Alt-Del sequence to the remoting client.
683 * @return {void} Nothing.
685 remoting.ClientSession.prototype.sendCtrlAltDel = function() {
686 console.log('Sending Ctrl-Alt-Del.');
687 this.sendKeyCombination_([0x0700e0, 0x0700e2, 0x07004c]);
691 * Sends a Print Screen keypress to the remoting client.
693 * @return {void} Nothing.
695 remoting.ClientSession.prototype.sendPrintScreen = function() {
696 console.log('Sending Print Screen.');
697 this.sendKeyCombination_([0x070046]);
701 * Sets and stores the key remapping setting for the current host.
703 * @param {string} remappings Comma separated list of key remappings.
705 remoting.ClientSession.prototype.setRemapKeys = function(remappings) {
706 // Cancel any existing remappings and apply the new ones.
707 this.applyRemapKeys_(false);
708 this.remapKeys_ = remappings;
709 this.applyRemapKeys_(true);
711 // Save the new remapping setting.
713 options[remoting.ClientSession.KEY_REMAP_KEYS] = this.remapKeys_;
714 remoting.HostSettings.save(this.hostId_, options);
718 * Applies the configured key remappings to the session, or resets them.
720 * @param {boolean} apply True to apply remappings, false to cancel them.
722 remoting.ClientSession.prototype.applyRemapKeys_ = function(apply) {
723 // By default, under ChromeOS, remap the right Control key to the right
725 var remapKeys = this.remapKeys_;
726 if (remapKeys == '' && remoting.runningOnChromeOS()) {
727 remapKeys = '0x0700e4>0x0700e7';
730 if (remapKeys == '') {
734 var remappings = remapKeys.split(',');
735 for (var i = 0; i < remappings.length; ++i) {
736 var keyCodes = remappings[i].split('>');
737 if (keyCodes.length != 2) {
738 console.log('bad remapKey: ' + remappings[i]);
741 var fromKey = parseInt(keyCodes[0], 0);
742 var toKey = parseInt(keyCodes[1], 0);
743 if (!fromKey || !toKey) {
744 console.log('bad remapKey code: ' + remappings[i]);
748 console.log('remapKey 0x' + fromKey.toString(16) +
749 '>0x' + toKey.toString(16));
750 this.plugin_.remapKey(fromKey, toKey);
752 console.log('cancel remapKey 0x' + fromKey.toString(16));
753 this.plugin_.remapKey(fromKey, fromKey);
759 * Set the shrink-to-fit and resize-to-client flags and save them if this is
760 * a Me2Me connection.
762 * @param {boolean} shrinkToFit True if the remote desktop should be scaled
763 * down if it is larger than the client window; false if scroll-bars
764 * should be added in this case.
765 * @param {boolean} resizeToClient True if window resizes should cause the
766 * host to attempt to resize its desktop to match the client window size;
767 * false to disable this behaviour for subsequent window resizes--the
768 * current host desktop size is not restored in this case.
769 * @return {void} Nothing.
771 remoting.ClientSession.prototype.setScreenMode =
772 function(shrinkToFit, resizeToClient) {
773 if (resizeToClient && !this.resizeToClient_) {
774 var clientArea = this.getClientArea_();
775 this.plugin_.notifyClientResolution(clientArea.width,
777 window.devicePixelRatio);
780 // If enabling shrink, reset bump-scroll offsets.
781 var needsScrollReset = shrinkToFit && !this.shrinkToFit_;
783 this.shrinkToFit_ = shrinkToFit;
784 this.resizeToClient_ = resizeToClient;
785 this.updateScrollbarVisibility();
787 if (this.hostId_ != '') {
789 options[remoting.ClientSession.KEY_SHRINK_TO_FIT] = this.shrinkToFit_;
790 options[remoting.ClientSession.KEY_RESIZE_TO_CLIENT] = this.resizeToClient_;
791 remoting.HostSettings.save(this.hostId_, options);
794 this.updateDimensions();
795 if (needsScrollReset) {
802 * Called when the client receives its first frame.
804 * @return {void} Nothing.
806 remoting.ClientSession.prototype.onFirstFrameReceived = function() {
807 this.hasReceivedFrame_ = true;
811 * @return {boolean} Whether the client has received a video buffer.
813 remoting.ClientSession.prototype.hasReceivedFrame = function() {
814 return this.hasReceivedFrame_;
818 * Sends an IQ stanza via the http xmpp proxy.
821 * @param {string} msg XML string of IQ stanza to send to server.
822 * @return {void} Nothing.
824 remoting.ClientSession.prototype.sendIq_ = function(msg) {
825 // Extract the session id, so we can close the session later.
826 var parser = new DOMParser();
827 var iqNode = parser.parseFromString(msg, 'text/xml').firstChild;
828 var jingleNode = iqNode.firstChild;
830 var action = jingleNode.getAttribute('action');
831 if (jingleNode.nodeName == 'jingle' && action == 'session-initiate') {
832 this.sessionId_ = jingleNode.getAttribute('sid');
836 // HACK: Add 'x' prefix to the IDs of the outgoing messages to make sure that
837 // stanza IDs used by host and client do not match. This is necessary to
838 // workaround bug in the signaling endpoint used by chromoting.
839 // TODO(sergeyu): Remove this hack once the server-side bug is fixed.
840 var type = iqNode.getAttribute('type');
842 var id = iqNode.getAttribute('id');
843 iqNode.setAttribute('id', 'x' + id);
844 msg = (new XMLSerializer()).serializeToString(iqNode);
847 console.log(remoting.timestamp(), remoting.formatIq.prettifySendIq(msg));
850 remoting.wcsSandbox.sendIq(msg);
853 remoting.ClientSession.prototype.initiateConnection_ = function() {
854 /** @type {remoting.ClientSession} */
857 remoting.wcsSandbox.connect(onWcsConnected, this.resetWithError_.bind(this));
859 /** @param {string} localJid Local JID. */
860 function onWcsConnected(localJid) {
861 that.connectPluginToWcs_(localJid);
862 that.getSharedSecret_(onSharedSecretReceived.bind(null, localJid));
865 /** @param {string} localJid Local JID.
866 * @param {string} sharedSecret Shared secret. */
867 function onSharedSecretReceived(localJid, sharedSecret) {
868 that.plugin_.connect(
869 that.hostJid_, that.hostPublicKey_, localJid, sharedSecret,
870 that.authenticationMethods_, that.hostId_, that.clientPairingId_,
871 that.clientPairedSecret_);
876 * Connects the plugin to WCS.
879 * @param {string} localJid Local JID.
880 * @return {void} Nothing.
882 remoting.ClientSession.prototype.connectPluginToWcs_ = function(localJid) {
883 remoting.formatIq.setJids(localJid, this.hostJid_);
884 var forwardIq = this.plugin_.onIncomingIq.bind(this.plugin_);
885 /** @param {string} stanza The IQ stanza received. */
886 var onIncomingIq = function(stanza) {
887 // HACK: Remove 'x' prefix added to the id in sendIq_().
889 var parser = new DOMParser();
890 var iqNode = parser.parseFromString(stanza, 'text/xml').firstChild;
891 var type = iqNode.getAttribute('type');
892 var id = iqNode.getAttribute('id');
893 if (type != 'set' && id.charAt(0) == 'x') {
894 iqNode.setAttribute('id', id.substr(1));
895 stanza = (new XMLSerializer()).serializeToString(iqNode);
898 // Pass message as is when it is malformed.
901 console.log(remoting.timestamp(),
902 remoting.formatIq.prettifyReceiveIq(stanza));
905 remoting.wcsSandbox.setOnIq(onIncomingIq);
909 * Gets shared secret to be used for connection.
911 * @param {function(string)} callback Callback called with the shared secret.
912 * @return {void} Nothing.
915 remoting.ClientSession.prototype.getSharedSecret_ = function(callback) {
916 /** @type remoting.ClientSession */
918 if (this.plugin_.hasFeature(remoting.ClientPlugin.Feature.THIRD_PARTY_AUTH)) {
919 /** @type{function(string, string, string): void} */
920 var fetchThirdPartyToken = function(tokenUrl, hostPublicKey, scope) {
921 that.fetchThirdPartyToken_(
922 tokenUrl, hostPublicKey, scope,
923 that.plugin_.onThirdPartyTokenFetched.bind(that.plugin_));
925 this.plugin_.fetchThirdPartyTokenHandler = fetchThirdPartyToken;
927 if (this.accessCode_) {
928 // Shared secret was already supplied before connecting (It2Me case).
929 callback(this.accessCode_);
930 } else if (this.plugin_.hasFeature(
931 remoting.ClientPlugin.Feature.ASYNC_PIN)) {
932 // Plugin supports asynchronously asking for the PIN.
933 this.plugin_.useAsyncPinDialog();
934 /** @param {boolean} pairingSupported */
935 var fetchPin = function(pairingSupported) {
936 that.fetchPin_(pairingSupported,
937 that.plugin_.onPinFetched.bind(that.plugin_));
939 this.plugin_.fetchPinHandler = fetchPin;
942 // Clients that don't support asking for a PIN asynchronously also don't
943 // support pairing, so request the PIN now without offering to remember it.
944 this.fetchPin_(false, callback);
949 * Callback that the plugin invokes to indicate that the connection
950 * status has changed.
953 * @param {number} status The plugin's status.
954 * @param {number} error The plugin's error state, if any.
956 remoting.ClientSession.prototype.onConnectionStatusUpdate_ =
957 function(status, error) {
958 if (status == remoting.ClientSession.State.CONNECTED) {
959 this.setFocusHandlers_();
960 this.onDesktopSizeChanged_();
961 if (this.resizeToClient_) {
962 var clientArea = this.getClientArea_();
963 this.plugin_.notifyClientResolution(clientArea.width,
965 window.devicePixelRatio);
967 // Activate full-screen related UX.
968 remoting.fullscreen.addListener(this.callOnFullScreenChanged_);
969 remoting.fullscreen.syncWithMaximize(true);
970 if (remoting.windowFrame) {
971 remoting.windowFrame.setClientSession(this);
973 remoting.toolbar.setClientSession(this);
976 this.container_.addEventListener('mousemove',
977 this.updateMouseCursorPosition_,
980 } else if (status == remoting.ClientSession.State.FAILED) {
982 case remoting.ClientSession.ConnectionError.HOST_IS_OFFLINE:
983 this.error_ = remoting.Error.HOST_IS_OFFLINE;
985 case remoting.ClientSession.ConnectionError.SESSION_REJECTED:
986 this.error_ = remoting.Error.INVALID_ACCESS_CODE;
988 case remoting.ClientSession.ConnectionError.INCOMPATIBLE_PROTOCOL:
989 this.error_ = remoting.Error.INCOMPATIBLE_PROTOCOL;
991 case remoting.ClientSession.ConnectionError.NETWORK_FAILURE:
992 this.error_ = remoting.Error.P2P_FAILURE;
994 case remoting.ClientSession.ConnectionError.HOST_OVERLOAD:
995 this.error_ = remoting.Error.HOST_OVERLOAD;
998 this.error_ = remoting.Error.UNEXPECTED;
1001 this.setState_(/** @type {remoting.ClientSession.State} */ (status));
1005 * Callback that the plugin invokes to indicate when the connection is
1009 * @param {boolean} ready True if the connection is ready.
1011 remoting.ClientSession.prototype.onConnectionReady_ = function(ready) {
1013 this.container_.classList.add('session-client-inactive');
1015 this.container_.classList.remove('session-client-inactive');
1018 this.raiseEvent(remoting.ClientSession.Events.videoChannelStateChanged,
1023 * Called when the client-host capabilities negotiation is complete.
1025 * @param {!Array.<string>} capabilities The set of capabilities negotiated
1026 * between the client and host.
1027 * @return {void} Nothing.
1030 remoting.ClientSession.prototype.onSetCapabilities_ = function(capabilities) {
1031 if (this.capabilities_ != null) {
1032 console.error('onSetCapabilities_() is called more than once');
1036 this.capabilities_ = capabilities;
1037 if (this.hasCapability_(
1038 remoting.ClientSession.Capability.SEND_INITIAL_RESOLUTION)) {
1039 var clientArea = this.getClientArea_();
1040 this.plugin_.notifyClientResolution(clientArea.width,
1042 window.devicePixelRatio);
1048 * @param {remoting.ClientSession.State} newState The new state for the session.
1049 * @return {void} Nothing.
1051 remoting.ClientSession.prototype.setState_ = function(newState) {
1052 var oldState = this.state_;
1053 this.state_ = newState;
1054 var state = this.state_;
1055 if (oldState == remoting.ClientSession.State.CONNECTING) {
1056 if (this.state_ == remoting.ClientSession.State.CLOSED) {
1057 state = remoting.ClientSession.State.CONNECTION_CANCELED;
1058 } else if (this.state_ == remoting.ClientSession.State.FAILED &&
1059 this.error_ == remoting.Error.HOST_IS_OFFLINE &&
1060 !this.logHostOfflineErrors_) {
1061 // The application requested host-offline errors to be suppressed, for
1062 // example, because this connection attempt is using a cached host JID.
1063 console.log('Suppressing host-offline error.');
1064 state = remoting.ClientSession.State.CONNECTION_CANCELED;
1066 } else if (oldState == remoting.ClientSession.State.CONNECTED &&
1067 this.state_ == remoting.ClientSession.State.FAILED) {
1068 state = remoting.ClientSession.State.CONNECTION_DROPPED;
1070 this.logToServer.logClientSessionStateChange(state, this.error_, this.mode_);
1071 if (this.state_ == remoting.ClientSession.State.CONNECTED) {
1072 this.createGnubbyAuthHandler_();
1075 this.raiseEvent(remoting.ClientSession.Events.stateChanged,
1076 new remoting.ClientSession.StateEvent(newState, oldState)
1081 * This is a callback that gets called when the window is resized.
1083 * @return {void} Nothing.
1085 remoting.ClientSession.prototype.onResize = function() {
1086 this.updateDimensions();
1088 if (this.notifyClientResolutionTimer_) {
1089 window.clearTimeout(this.notifyClientResolutionTimer_);
1090 this.notifyClientResolutionTimer_ = null;
1093 // Defer notifying the host of the change until the window stops resizing, to
1094 // avoid overloading the control channel with notifications.
1095 if (this.resizeToClient_) {
1096 var kResizeRateLimitMs = 1000;
1097 if (this.hasCapability_(
1098 remoting.ClientSession.Capability.RATE_LIMIT_RESIZE_REQUESTS)) {
1099 kResizeRateLimitMs = 250;
1101 var clientArea = this.getClientArea_();
1102 this.notifyClientResolutionTimer_ = window.setTimeout(
1103 this.plugin_.notifyClientResolution.bind(this.plugin_,
1106 window.devicePixelRatio),
1107 kResizeRateLimitMs);
1110 // If bump-scrolling is enabled, adjust the plugin margins to fully utilize
1111 // the new window area.
1112 this.resetScroll_();
1114 this.updateScrollbarVisibility();
1118 * Requests that the host pause or resume video updates.
1120 * @param {boolean} pause True to pause video, false to resume.
1121 * @return {void} Nothing.
1123 remoting.ClientSession.prototype.pauseVideo = function(pause) {
1125 this.plugin_.pauseVideo(pause);
1130 * Requests that the host pause or resume audio.
1132 * @param {boolean} pause True to pause audio, false to resume.
1133 * @return {void} Nothing.
1135 remoting.ClientSession.prototype.pauseAudio = function(pause) {
1137 this.plugin_.pauseAudio(pause)
1142 * This is a callback that gets called when the plugin notifies us of a change
1143 * in the size of the remote desktop.
1146 * @return {void} Nothing.
1148 remoting.ClientSession.prototype.onDesktopSizeChanged_ = function() {
1149 console.log('desktop size changed: ' +
1150 this.plugin_.desktopWidth + 'x' +
1151 this.plugin_.desktopHeight +' @ ' +
1152 this.plugin_.desktopXDpi + 'x' +
1153 this.plugin_.desktopYDpi + ' DPI');
1154 this.updateDimensions();
1155 this.updateScrollbarVisibility();
1159 * Refreshes the plugin's dimensions, taking into account the sizes of the
1160 * remote desktop and client window, and the current scale-to-fit setting.
1162 * @return {void} Nothing.
1164 remoting.ClientSession.prototype.updateDimensions = function() {
1165 if (this.plugin_.desktopWidth == 0 ||
1166 this.plugin_.desktopHeight == 0) {
1170 var clientArea = this.getClientArea_();
1171 var desktopWidth = this.plugin_.desktopWidth;
1172 var desktopHeight = this.plugin_.desktopHeight;
1174 // When configured to display a host at its original size, we aim to display
1175 // it as close to its physical size as possible, without losing data:
1176 // - If client and host have matching DPI, render the host pixel-for-pixel.
1177 // - If the host has higher DPI then still render pixel-for-pixel.
1178 // - If the host has lower DPI then let Chrome up-scale it to natural size.
1180 // We specify the plugin dimensions in Density-Independent Pixels, so to
1181 // render pixel-for-pixel we need to down-scale the host dimensions by the
1182 // devicePixelRatio of the client. To match the host pixel density, we choose
1183 // an initial scale factor based on the client devicePixelRatio and host DPI.
1185 // Determine the effective device pixel ratio of the host, based on DPI.
1186 var hostPixelRatioX = Math.ceil(this.plugin_.desktopXDpi / 96);
1187 var hostPixelRatioY = Math.ceil(this.plugin_.desktopYDpi / 96);
1188 var hostPixelRatio = Math.min(hostPixelRatioX, hostPixelRatioY);
1190 // Down-scale by the smaller of the client and host ratios.
1191 var scale = 1.0 / Math.min(window.devicePixelRatio, hostPixelRatio);
1193 if (this.shrinkToFit_) {
1194 // Reduce the scale, if necessary, to fit the whole desktop in the window.
1195 var scaleFitWidth = Math.min(scale, 1.0 * clientArea.width / desktopWidth);
1196 var scaleFitHeight =
1197 Math.min(scale, 1.0 * clientArea.height / desktopHeight);
1198 scale = Math.min(scaleFitHeight, scaleFitWidth);
1200 // If we're running full-screen then try to handle common side-by-side
1201 // multi-monitor combinations more intelligently.
1202 if (remoting.fullscreen.isActive()) {
1203 // If the host has two monitors each the same size as the client then
1204 // scale-to-fit will have the desktop occupy only 50% of the client area,
1205 // in which case it would be preferable to down-scale less and let the
1206 // user bump-scroll around ("scale-and-pan").
1207 // Triggering scale-and-pan if less than 65% of the client area would be
1208 // used adds enough fuzz to cope with e.g. 1280x800 client connecting to
1209 // a (2x1280)x1024 host nicely.
1210 // Note that we don't need to account for scrollbars while fullscreen.
1211 if (scale <= scaleFitHeight * 0.65) {
1212 scale = scaleFitHeight;
1214 if (scale <= scaleFitWidth * 0.65) {
1215 scale = scaleFitWidth;
1220 var pluginWidth = Math.round(desktopWidth * scale);
1221 var pluginHeight = Math.round(desktopHeight * scale);
1224 this.video_.style.width = pluginWidth + 'px';
1225 this.video_.style.height = pluginHeight + 'px';
1228 // Resize the plugin if necessary.
1229 // TODO(wez): Handle high-DPI to high-DPI properly (crbug.com/135089).
1230 this.plugin_.element().style.width = pluginWidth + 'px';
1231 this.plugin_.element().style.height = pluginHeight + 'px';
1233 // Position the container.
1234 // Note that clientWidth/Height take into account scrollbars.
1235 var clientWidth = document.documentElement.clientWidth;
1236 var clientHeight = document.documentElement.clientHeight;
1237 var parentNode = this.plugin_.element().parentNode;
1239 console.log('plugin dimensions: ' +
1240 parentNode.style.left + ',' +
1241 parentNode.style.top + '-' +
1242 pluginWidth + 'x' + pluginHeight + '.');
1246 * Returns an associative array with a set of stats for this connection.
1248 * @return {remoting.ClientSession.PerfStats} The connection statistics.
1250 remoting.ClientSession.prototype.getPerfStats = function() {
1251 return this.plugin_.getPerfStats();
1257 * @param {remoting.ClientSession.PerfStats} stats
1259 remoting.ClientSession.prototype.logStatistics = function(stats) {
1260 this.logToServer.logStatistics(stats, this.mode_);
1264 * Enable or disable logging of connection errors due to a host being offline.
1265 * For example, if attempting a connection using a cached JID, host-offline
1266 * errors should not be logged because the JID will be refreshed and the
1267 * connection retried.
1269 * @param {boolean} enable True to log host-offline errors; false to suppress.
1271 remoting.ClientSession.prototype.logHostOfflineErrors = function(enable) {
1272 this.logHostOfflineErrors_ = enable;
1276 * Request pairing with the host for PIN-less authentication.
1278 * @param {string} clientName The human-readable name of the client.
1279 * @param {function(string, string):void} onDone Callback to receive the
1280 * client id and shared secret when they are available.
1282 remoting.ClientSession.prototype.requestPairing = function(clientName, onDone) {
1284 this.plugin_.requestPairing(clientName, onDone);
1289 * Called when the full-screen status has changed, either via the
1290 * remoting.Fullscreen class, or via a system event such as the Escape key
1292 * @param {boolean} fullscreen True if the app is entering full-screen mode;
1293 * false if it is leaving it.
1296 remoting.ClientSession.prototype.onFullScreenChanged_ = function (fullscreen) {
1297 var htmlNode = /** @type {HTMLElement} */ (document.documentElement);
1298 this.enableBumpScroll_(fullscreen);
1300 htmlNode.classList.add('full-screen');
1302 htmlNode.classList.remove('full-screen');
1307 * Updates the options menu to reflect the current scale-to-fit and full-screen
1309 * @return {void} Nothing.
1312 remoting.ClientSession.prototype.onShowOptionsMenu_ = function() {
1313 remoting.MenuButton.select(this.resizeToClientButton_, this.resizeToClient_);
1314 remoting.MenuButton.select(this.shrinkToFitButton_, this.shrinkToFit_);
1315 remoting.MenuButton.select(this.fullScreenButton_,
1316 remoting.fullscreen.isActive());
1320 * Scroll the client plugin by the specified amount, keeping it visible.
1321 * Note that this is only used in content full-screen mode (not windowed or
1322 * browser full-screen modes), where window.scrollBy and the scrollTop and
1323 * scrollLeft properties don't work.
1324 * @param {number} dx The amount by which to scroll horizontally. Positive to
1325 * scroll right; negative to scroll left.
1326 * @param {number} dy The amount by which to scroll vertically. Positive to
1327 * scroll down; negative to scroll up.
1328 * @return {boolean} True if the requested scroll had no effect because both
1329 * vertical and horizontal edges of the screen have been reached.
1332 remoting.ClientSession.prototype.scroll_ = function(dx, dy) {
1333 var plugin = this.plugin_.element();
1334 var style = plugin.style;
1337 * Helper function for x- and y-scrolling
1338 * @param {number|string} curr The current margin, eg. "10px".
1339 * @param {number} delta The requested scroll amount.
1340 * @param {number} windowBound The size of the window, in pixels.
1341 * @param {number} pluginBound The size of the plugin, in pixels.
1342 * @param {{stop: boolean}} stop Reference parameter used to indicate when
1343 * the scroll has reached one of the edges and can be stopped in that
1345 * @return {string} The new margin value.
1347 var adjustMargin = function(curr, delta, windowBound, pluginBound, stop) {
1348 var minMargin = Math.min(0, windowBound - pluginBound);
1349 var result = (curr ? parseFloat(curr) : 0) - delta;
1350 result = Math.min(0, Math.max(minMargin, result));
1351 stop.stop = (result == 0 || result == minMargin);
1352 return result + 'px';
1355 var stopX = { stop: false };
1356 var clientArea = this.getClientArea_();
1357 style.marginLeft = adjustMargin(style.marginLeft, dx, clientArea.width,
1358 this.pluginWidthForBumpScrollTesting || plugin.clientWidth, stopX);
1360 var stopY = { stop: false };
1361 style.marginTop = adjustMargin(
1362 style.marginTop, dy, clientArea.height,
1363 this.pluginHeightForBumpScrollTesting || plugin.clientHeight, stopY);
1364 return stopX.stop && stopY.stop;
1367 remoting.ClientSession.prototype.resetScroll_ = function() {
1369 var plugin = this.plugin_.element();
1370 plugin.style.marginTop = '0px';
1371 plugin.style.marginLeft = '0px';
1376 * Enable or disable bump-scrolling. When disabling bump scrolling, also reset
1377 * the scroll offsets to (0, 0).
1379 * @param {boolean} enable True to enable bump-scrolling, false to disable it.
1381 remoting.ClientSession.prototype.enableBumpScroll_ = function(enable) {
1382 var element = /*@type{HTMLElement} */ document.documentElement;
1384 /** @type {null|function(Event):void} */
1385 this.onMouseMoveRef_ = this.onMouseMove_.bind(this);
1386 element.addEventListener('mousemove', this.onMouseMoveRef_, false);
1388 element.removeEventListener('mousemove', this.onMouseMoveRef_, false);
1389 this.onMouseMoveRef_ = null;
1390 this.resetScroll_();
1395 * @param {Event} event The mouse event.
1398 remoting.ClientSession.prototype.onMouseMove_ = function(event) {
1399 if (this.bumpScrollTimer_) {
1400 window.clearTimeout(this.bumpScrollTimer_);
1401 this.bumpScrollTimer_ = null;
1405 * Compute the scroll speed based on how close the mouse is to the edge.
1406 * @param {number} mousePos The mouse x- or y-coordinate
1407 * @param {number} size The width or height of the content area.
1408 * @return {number} The scroll delta, in pixels.
1410 var computeDelta = function(mousePos, size) {
1412 if (mousePos >= size - threshold) {
1413 return 1 + 5 * (mousePos - (size - threshold)) / threshold;
1414 } else if (mousePos <= threshold) {
1415 return -1 - 5 * (threshold - mousePos) / threshold;
1420 var clientArea = this.getClientArea_();
1421 var dx = computeDelta(event.x, clientArea.width);
1422 var dy = computeDelta(event.y, clientArea.height);
1424 if (dx != 0 || dy != 0) {
1425 this.raiseEvent(remoting.ClientSession.Events.bumpScrollStarted);
1426 /** @type {remoting.ClientSession} */
1429 * Scroll the view, and schedule a timer to do so again unless we've hit
1430 * the edges of the screen. This timer is cancelled when the mouse moves.
1431 * @param {number} expected The time at which we expect to be called.
1433 var repeatScroll = function(expected) {
1434 /** @type {number} */
1435 var now = new Date().getTime();
1436 /** @type {number} */
1438 var lateAdjustment = 1 + (now - expected) / timeout;
1439 if (that.scroll_(lateAdjustment * dx, lateAdjustment * dy)) {
1440 that.raiseEvent(remoting.ClientSession.Events.bumpScrollStopped);
1442 that.bumpScrollTimer_ = window.setTimeout(
1443 function() { repeatScroll(now + timeout); },
1447 repeatScroll(new Date().getTime());
1452 * Sends a clipboard item to the host.
1454 * @param {string} mimeType The MIME type of the clipboard item.
1455 * @param {string} item The clipboard item.
1457 remoting.ClientSession.prototype.sendClipboardItem = function(mimeType, item) {
1460 this.plugin_.sendClipboardItem(mimeType, item);
1464 * Send a gnubby-auth extension message to the host.
1465 * @param {Object} data The gnubby-auth message data.
1467 remoting.ClientSession.prototype.sendGnubbyAuthMessage = function(data) {
1470 this.plugin_.sendClientMessage('gnubby-auth', JSON.stringify(data));
1474 * Process a remote gnubby auth request.
1475 * @param {string} data Remote gnubby request data.
1478 remoting.ClientSession.prototype.processGnubbyAuthMessage_ = function(data) {
1479 if (this.gnubbyAuthHandler_) {
1481 this.gnubbyAuthHandler_.onMessage(data);
1483 console.error('Failed to process gnubby message: ',
1484 /** @type {*} */ (err));
1487 console.error('Received unexpected gnubby message');
1492 * Create a gnubby auth handler and inform the host that gnubby auth is
1496 remoting.ClientSession.prototype.createGnubbyAuthHandler_ = function() {
1497 if (this.mode_ == remoting.ClientSession.Mode.ME2ME) {
1498 this.gnubbyAuthHandler_ = new remoting.GnubbyAuthHandler(this);
1499 // TODO(psj): Move to more generic capabilities mechanism.
1500 this.sendGnubbyAuthMessage({'type': 'control', 'option': 'auth-v1'});
1505 * @return {{width: number, height: number}} The height of the window's client
1506 * area. This differs between apps v1 and apps v2 due to the custom window
1507 * borders used by the latter.
1510 remoting.ClientSession.prototype.getClientArea_ = function() {
1511 return remoting.windowFrame ?
1512 remoting.windowFrame.getClientArea() :
1513 { 'width': window.innerWidth, 'height': window.innerHeight };
1517 * @param {string} url
1518 * @param {number} hotspotX
1519 * @param {number} hotspotY
1521 remoting.ClientSession.prototype.updateMouseCursorImage_ =
1522 function(url, hotspotX, hotspotY) {
1523 this.mouseCursorOverlay_.hidden = !url;
1525 this.mouseCursorOverlay_.style.marginLeft = '-' + hotspotX + 'px';
1526 this.mouseCursorOverlay_.style.marginTop = '-' + hotspotY + 'px';
1527 this.mouseCursorOverlay_.src = url;
1532 * @return {{top: number, left:number}} The top-left corner of the plugin.
1534 remoting.ClientSession.prototype.getPluginPositionForTesting = function() {
1535 var plugin = this.plugin_.element();
1536 var style = plugin.style;
1538 top: parseFloat(style.marginTop),
1539 left: parseFloat(style.marginLeft)