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 {string} accessCode The IT2Me access code. Blank for Me2Me.
27 * @param {function(boolean, function(string): void): void} fetchPin
28 * Called by Me2Me connections when a PIN needs to be obtained
30 * @param {function(string, string, string,
31 * function(string, string): void): void}
32 * fetchThirdPartyToken Called by Me2Me connections when a third party
33 * authentication token must be obtained.
34 * @param {string} authenticationMethods Comma-separated list of
35 * authentication methods the client should attempt to use.
36 * @param {string} hostId The host identifier for Me2Me, or empty for IT2Me.
37 * Mixed into authentication hashes for some authentication methods.
38 * @param {string} hostJid The jid of the host to connect to.
39 * @param {string} hostPublicKey The base64 encoded version of the host's
41 * @param {remoting.ClientSession.Mode} mode The mode of this connection.
42 * @param {string} clientPairingId For paired Me2Me connections, the
43 * pairing id for this client, as issued by the host.
44 * @param {string} clientPairedSecret For paired Me2Me connections, the
45 * paired secret for this client, as issued by the host.
47 * @extends {base.EventSource}
49 remoting.ClientSession = function(accessCode, fetchPin, fetchThirdPartyToken,
50 authenticationMethods,
51 hostId, hostJid, hostPublicKey, mode,
52 clientPairingId, clientPairedSecret) {
54 this.state_ = remoting.ClientSession.State.CREATED;
57 this.error_ = remoting.Error.NONE;
60 this.hostJid_ = hostJid;
62 this.hostPublicKey_ = hostPublicKey;
64 this.accessCode_ = accessCode;
66 this.fetchPin_ = fetchPin;
68 this.fetchThirdPartyToken_ = fetchThirdPartyToken;
70 this.authenticationMethods_ = authenticationMethods;
72 this.hostId_ = hostId;
76 this.clientPairingId_ = clientPairingId;
78 this.clientPairedSecret_ = clientPairedSecret;
81 /** @type {remoting.ClientPlugin}
85 this.shrinkToFit_ = true;
87 this.resizeToClient_ = true;
91 this.hasReceivedFrame_ = false;
92 this.logToServer = new remoting.LogToServer();
94 /** @type {number?} @private */
95 this.notifyClientResolutionTimer_ = null;
96 /** @type {number?} @private */
97 this.bumpScrollTimer_ = null;
100 * Allow host-offline error reporting to be suppressed in situations where it
101 * would not be useful, for example, when using a cached host JID.
103 * @type {boolean} @private
105 this.logHostOfflineErrors_ = true;
108 this.callPluginLostFocus_ = this.pluginLostFocus_.bind(this);
110 this.callPluginGotFocus_ = this.pluginGotFocus_.bind(this);
112 this.callSetScreenMode_ = this.onSetScreenMode_.bind(this);
114 this.callToggleFullScreen_ = remoting.fullscreen.toggle.bind(
115 remoting.fullscreen);
117 this.callOnFullScreenChanged_ = this.onFullScreenChanged_.bind(this)
120 this.screenOptionsMenu_ = new remoting.MenuButton(
121 document.getElementById('screen-options-menu'),
122 this.onShowOptionsMenu_.bind(this));
124 this.sendKeysMenu_ = new remoting.MenuButton(
125 document.getElementById('send-keys-menu')
128 /** @type {HTMLMediaElement} @private */
131 /** @type {Element} @private */
132 this.mouseCursorOverlay_ =
133 this.container_.querySelector('.mouse-cursor-overlay');
135 /** @type {Element} */
136 var img = this.mouseCursorOverlay_;
137 /** @param {Event} event @private */
138 this.updateMouseCursorPosition_ = function(event) {
139 img.style.top = event.y + 'px';
140 img.style.left = event.x + 'px';
143 /** @type {HTMLElement} @private */
144 this.resizeToClientButton_ =
145 document.getElementById('screen-resize-to-client');
146 /** @type {HTMLElement} @private */
147 this.shrinkToFitButton_ = document.getElementById('screen-shrink-to-fit');
148 /** @type {HTMLElement} @private */
149 this.fullScreenButton_ = document.getElementById('toggle-full-screen');
151 /** @type {remoting.GnubbyAuthHandler} @private */
152 this.gnubbyAuthHandler_ = null;
154 if (this.mode_ == remoting.ClientSession.Mode.IT2ME) {
155 // Resize-to-client is not supported for IT2Me hosts.
156 this.resizeToClientButton_.hidden = true;
158 this.resizeToClientButton_.hidden = false;
159 this.resizeToClientButton_.addEventListener(
160 'click', this.callSetScreenMode_, false);
163 this.shrinkToFitButton_.addEventListener(
164 'click', this.callSetScreenMode_, false);
165 this.fullScreenButton_.addEventListener(
166 'click', this.callToggleFullScreen_, false);
167 this.defineEvents(Object.keys(remoting.ClientSession.Events));
170 base.extend(remoting.ClientSession, base.EventSource);
172 /** @enum {string} */
173 remoting.ClientSession.Events = {
174 stateChanged: 'stateChanged',
175 videoChannelStateChanged: 'videoChannelStateChanged'
179 * Called when the window or desktop size or the scaling settings change,
180 * to set the scroll-bar visibility.
182 * TODO(jamiewalch): crbug.com/252796: Remove this once crbug.com/240772 is
185 remoting.ClientSession.prototype.updateScrollbarVisibility = function() {
186 var needsVerticalScroll = false;
187 var needsHorizontalScroll = false;
188 if (!this.shrinkToFit_) {
189 // Determine whether or not horizontal or vertical scrollbars are
190 // required, taking into account their width.
191 var clientArea = this.getClientArea_();
192 needsVerticalScroll = clientArea.height < this.plugin_.desktopHeight;
193 needsHorizontalScroll = clientArea.width < this.plugin_.desktopWidth;
194 var kScrollBarWidth = 16;
195 if (needsHorizontalScroll && !needsVerticalScroll) {
196 needsVerticalScroll =
197 clientArea.height - kScrollBarWidth < this.plugin_.desktopHeight;
198 } else if (!needsHorizontalScroll && needsVerticalScroll) {
199 needsHorizontalScroll =
200 clientArea.width - kScrollBarWidth < this.plugin_.desktopWidth;
204 var scroller = document.getElementById('scroller');
205 if (needsHorizontalScroll) {
206 scroller.classList.remove('no-horizontal-scroll');
208 scroller.classList.add('no-horizontal-scroll');
210 if (needsVerticalScroll) {
211 scroller.classList.remove('no-vertical-scroll');
213 scroller.classList.add('no-vertical-scroll');
217 // Note that the positive values in both of these enums are copied directly
218 // from chromoting_scriptable_object.h and must be kept in sync. The negative
219 // values represent state transitions that occur within the web-app that have
220 // no corresponding plugin state transition.
221 /** @enum {number} */
222 remoting.ClientSession.State = {
223 CONNECTION_CANCELED: -3, // Connection closed (gracefully) before connecting.
224 CONNECTION_DROPPED: -2, // Succeeded, but subsequently closed with an error.
235 * @param {string} state The state name.
236 * @return {remoting.ClientSession.State} The session state enum value.
238 remoting.ClientSession.State.fromString = function(state) {
239 if (!remoting.ClientSession.State.hasOwnProperty(state)) {
240 throw "Invalid ClientSession.State: " + state;
242 return remoting.ClientSession.State[state];
247 @param {remoting.ClientSession.State} current
248 @param {remoting.ClientSession.State} previous
250 remoting.ClientSession.StateEvent = function(current, previous) {
251 /** @type {remoting.ClientSession.State} */
252 this.previous = previous
254 /** @type {remoting.ClientSession.State} */
255 this.current = current;
258 /** @enum {number} */
259 remoting.ClientSession.ConnectionError = {
264 INCOMPATIBLE_PROTOCOL: 3,
270 * @param {string} error The connection error name.
271 * @return {remoting.ClientSession.ConnectionError} The connection error enum.
273 remoting.ClientSession.ConnectionError.fromString = function(error) {
274 if (!remoting.ClientSession.ConnectionError.hasOwnProperty(error)) {
275 console.error('Unexpected ClientSession.ConnectionError string: ', error);
276 return remoting.ClientSession.ConnectionError.UNKNOWN;
278 return remoting.ClientSession.ConnectionError[error];
281 // The mode of this session.
282 /** @enum {number} */
283 remoting.ClientSession.Mode = {
289 * Type used for performance statistics collected by the plugin.
292 remoting.ClientSession.PerfStats = function() {};
293 /** @type {number} */
294 remoting.ClientSession.PerfStats.prototype.videoBandwidth;
295 /** @type {number} */
296 remoting.ClientSession.PerfStats.prototype.videoFrameRate;
297 /** @type {number} */
298 remoting.ClientSession.PerfStats.prototype.captureLatency;
299 /** @type {number} */
300 remoting.ClientSession.PerfStats.prototype.encodeLatency;
301 /** @type {number} */
302 remoting.ClientSession.PerfStats.prototype.decodeLatency;
303 /** @type {number} */
304 remoting.ClientSession.PerfStats.prototype.renderLatency;
305 /** @type {number} */
306 remoting.ClientSession.PerfStats.prototype.roundtripLatency;
308 // Keys for connection statistics.
309 remoting.ClientSession.STATS_KEY_VIDEO_BANDWIDTH = 'videoBandwidth';
310 remoting.ClientSession.STATS_KEY_VIDEO_FRAME_RATE = 'videoFrameRate';
311 remoting.ClientSession.STATS_KEY_CAPTURE_LATENCY = 'captureLatency';
312 remoting.ClientSession.STATS_KEY_ENCODE_LATENCY = 'encodeLatency';
313 remoting.ClientSession.STATS_KEY_DECODE_LATENCY = 'decodeLatency';
314 remoting.ClientSession.STATS_KEY_RENDER_LATENCY = 'renderLatency';
315 remoting.ClientSession.STATS_KEY_ROUNDTRIP_LATENCY = 'roundtripLatency';
317 // Keys for per-host settings.
318 remoting.ClientSession.KEY_REMAP_KEYS = 'remapKeys';
319 remoting.ClientSession.KEY_RESIZE_TO_CLIENT = 'resizeToClient';
320 remoting.ClientSession.KEY_SHRINK_TO_FIT = 'shrinkToFit';
323 * The id of the client plugin
327 remoting.ClientSession.prototype.PLUGIN_ID = 'session-client-plugin';
330 * Set of capabilities for which hasCapability_() can be used to test.
334 remoting.ClientSession.Capability = {
335 // When enabled this capability causes the client to send its screen
336 // resolution to the host once connection has been established. See
337 // this.plugin_.notifyClientResolution().
338 SEND_INITIAL_RESOLUTION: 'sendInitialResolution',
339 RATE_LIMIT_RESIZE_REQUESTS: 'rateLimitResizeRequests'
343 * The set of capabilities negotiated between the client and host.
344 * @type {Array.<string>}
347 remoting.ClientSession.prototype.capabilities_ = null;
350 * @param {remoting.ClientSession.Capability} capability The capability to test
352 * @return {boolean} True if the capability has been negotiated between
353 * the client and host.
356 remoting.ClientSession.prototype.hasCapability_ = function(capability) {
357 if (this.capabilities_ == null)
360 return this.capabilities_.indexOf(capability) > -1;
364 * @param {Element} container The element to add the plugin to.
365 * @param {string} id Id to use for the plugin element .
366 * @param {function(string, string):boolean} onExtensionMessage The handler for
367 * protocol extension messages. Returns true if a message is recognized;
369 * @return {remoting.ClientPlugin} Create plugin object for the locally
372 remoting.ClientSession.prototype.createClientPlugin_ =
373 function(container, id, onExtensionMessage) {
374 var plugin = /** @type {remoting.ViewerPlugin} */
375 document.createElement('embed');
378 if (remoting.settings.CLIENT_PLUGIN_TYPE == 'pnacl') {
379 plugin.src = 'remoting_client_pnacl.nmf';
380 plugin.type = 'application/x-pnacl';
381 } else if (remoting.settings.CLIENT_PLUGIN_TYPE == 'nacl') {
382 plugin.src = 'remoting_client_nacl.nmf';
383 plugin.type = 'application/x-nacl';
385 plugin.src = 'about://none';
386 plugin.type = 'application/vnd.chromium.remoting-viewer';
391 plugin.tabIndex = 0; // Required, otherwise focus() doesn't work.
392 container.appendChild(plugin);
394 return new remoting.ClientPlugin(plugin, onExtensionMessage);
398 * Callback function called when the plugin element gets focus.
400 remoting.ClientSession.prototype.pluginGotFocus_ = function() {
401 remoting.clipboard.initiateToHost();
405 * Callback function called when the plugin element loses focus.
407 remoting.ClientSession.prototype.pluginLostFocus_ = function() {
409 // Release all keys to prevent them becoming 'stuck down' on the host.
410 this.plugin_.releaseAllKeys();
411 if (this.plugin_.element()) {
412 // Focus should stay on the element, not (for example) the toolbar.
413 // Due to crbug.com/246335, we can't restore the focus immediately,
414 // otherwise the plugin gets confused about whether or not it has focus.
416 this.plugin_.element().focus.bind(this.plugin_.element()),
423 * Adds <embed> element to |container| and readies the sesion object.
425 * @param {Element} container The element to add the plugin to.
426 * @param {function(string, string):boolean} onExtensionMessage The handler for
427 * protocol extension messages. Returns true if a message is recognized;
430 remoting.ClientSession.prototype.createPluginAndConnect =
431 function(container, onExtensionMessage) {
432 this.plugin_ = this.createClientPlugin_(container, this.PLUGIN_ID,
434 remoting.HostSettings.load(this.hostId_,
435 this.onHostSettingsLoaded_.bind(this));
439 * @param {Object.<string>} options The current options for the host, or {}
440 * if this client has no saved settings for the host.
443 remoting.ClientSession.prototype.onHostSettingsLoaded_ = function(options) {
444 if (remoting.ClientSession.KEY_REMAP_KEYS in options &&
445 typeof(options[remoting.ClientSession.KEY_REMAP_KEYS]) ==
447 this.remapKeys_ = /** @type {string} */
448 options[remoting.ClientSession.KEY_REMAP_KEYS];
450 if (remoting.ClientSession.KEY_RESIZE_TO_CLIENT in options &&
451 typeof(options[remoting.ClientSession.KEY_RESIZE_TO_CLIENT]) ==
453 this.resizeToClient_ = /** @type {boolean} */
454 options[remoting.ClientSession.KEY_RESIZE_TO_CLIENT];
456 if (remoting.ClientSession.KEY_SHRINK_TO_FIT in options &&
457 typeof(options[remoting.ClientSession.KEY_SHRINK_TO_FIT]) ==
459 this.shrinkToFit_ = /** @type {boolean} */
460 options[remoting.ClientSession.KEY_SHRINK_TO_FIT];
463 /** @param {boolean} result */
464 this.plugin_.initialize(this.onPluginInitialized_.bind(this));
468 * Constrains the focus to the plugin element.
471 remoting.ClientSession.prototype.setFocusHandlers_ = function() {
472 this.plugin_.element().addEventListener(
473 'focus', this.callPluginGotFocus_, false);
474 this.plugin_.element().addEventListener(
475 'blur', this.callPluginLostFocus_, false);
476 this.plugin_.element().focus();
480 * @param {remoting.Error} error
482 remoting.ClientSession.prototype.resetWithError_ = function(error) {
483 this.plugin_.cleanup();
486 this.setState_(remoting.ClientSession.State.FAILED);
490 * @param {boolean} initialized
492 remoting.ClientSession.prototype.onPluginInitialized_ = function(initialized) {
494 console.error('ERROR: remoting plugin not loaded');
495 this.resetWithError_(remoting.Error.MISSING_PLUGIN);
499 if (!this.plugin_.isSupportedVersion()) {
500 this.resetWithError_(remoting.Error.BAD_PLUGIN_VERSION);
504 // Show the Send Keys menu only if the plugin has the injectKeyEvent feature,
505 // and the Ctrl-Alt-Del button only in Me2Me mode.
506 if (!this.plugin_.hasFeature(
507 remoting.ClientPlugin.Feature.INJECT_KEY_EVENT)) {
508 var sendKeysElement = document.getElementById('send-keys-menu');
509 sendKeysElement.hidden = true;
510 } else if (this.mode_ != remoting.ClientSession.Mode.ME2ME) {
511 var sendCadElement = document.getElementById('send-ctrl-alt-del');
512 sendCadElement.hidden = true;
515 // Apply customized key remappings if the plugin supports remapKeys.
516 if (this.plugin_.hasFeature(remoting.ClientPlugin.Feature.REMAP_KEY)) {
517 this.applyRemapKeys_(true);
521 // Enable MediaSource-based rendering on Chrome 37 and above.
522 var chromeVersionMajor =
523 parseInt((remoting.getChromeVersion() || '0').split('.')[0], 10);
524 if (chromeVersionMajor >= 37 &&
525 this.plugin_.hasFeature(
526 remoting.ClientPlugin.Feature.MEDIA_SOURCE_RENDERING)) {
527 this.video_ = /** @type {HTMLMediaElement} */(
528 document.getElementById('mediasource-video-output'));
529 // Make sure that the <video> element is hidden until we get the first
531 this.video_.style.width = '0px';
532 this.video_.style.height = '0px';
534 var renderer = new remoting.MediaSourceRenderer(this.video_);
535 this.plugin_.enableMediaSourceRendering(renderer);
536 /** @type {HTMLElement} */(document.getElementById('video-container'))
537 .classList.add('mediasource-rendering');
539 /** @type {HTMLElement} */(document.getElementById('video-container'))
540 .classList.remove('mediasource-rendering');
543 /** @param {string} msg The IQ stanza to send. */
544 this.plugin_.onOutgoingIqHandler = this.sendIq_.bind(this);
545 /** @param {string} msg The message to log. */
546 this.plugin_.onDebugMessageHandler = function(msg) {
547 console.log('plugin: ' + msg.trimRight());
550 this.plugin_.onConnectionStatusUpdateHandler =
551 this.onConnectionStatusUpdate_.bind(this);
552 this.plugin_.onConnectionReadyHandler = this.onConnectionReady_.bind(this);
553 this.plugin_.onDesktopSizeUpdateHandler =
554 this.onDesktopSizeChanged_.bind(this);
555 this.plugin_.onSetCapabilitiesHandler = this.onSetCapabilities_.bind(this);
556 this.plugin_.onGnubbyAuthHandler = this.processGnubbyAuthMessage_.bind(this);
557 this.plugin_.updateMouseCursorImage = this.updateMouseCursorImage_.bind(this);
558 this.initiateConnection_();
562 * Deletes the <embed> element from the container, without sending a
563 * session_terminate request. This is to be called when the session was
564 * disconnected by the Host.
566 * @return {void} Nothing.
568 remoting.ClientSession.prototype.removePlugin = function() {
570 this.plugin_.element().removeEventListener(
571 'focus', this.callPluginGotFocus_, false);
572 this.plugin_.element().removeEventListener(
573 'blur', this.callPluginLostFocus_, false);
574 this.plugin_.cleanup();
578 // Delete event handlers that aren't relevent when not connected.
579 this.resizeToClientButton_.removeEventListener(
580 'click', this.callSetScreenMode_, false);
581 this.shrinkToFitButton_.removeEventListener(
582 'click', this.callSetScreenMode_, false);
583 this.fullScreenButton_.removeEventListener(
584 'click', this.callToggleFullScreen_, false);
586 // Leave full-screen mode, and stop listening for related events.
587 var listener = this.callOnFullScreenChanged_;
588 remoting.fullscreen.syncWithMaximize(false);
589 remoting.fullscreen.activate(
592 remoting.fullscreen.removeListener(listener);
594 if (remoting.windowFrame) {
595 remoting.windowFrame.setConnected(false);
598 // Remove mediasource-rendering class from video-contained - this will also
599 // hide the <video> element.
600 /** @type {HTMLElement} */(document.getElementById('video-container'))
601 .classList.remove('mediasource-rendering');
603 this.container_.removeEventListener('mousemove',
604 this.updateMouseCursorPosition_,
609 * Disconnect the current session with a particular |error|. The session will
610 * raise a |stateChanged| event in response to it. The caller should then call
611 * |cleanup| to remove and destroy the <embed> element.
613 * @param {remoting.Error} error The reason for the disconnection. Use
614 * remoting.Error.NONE if there is no error.
615 * @return {void} Nothing.
617 remoting.ClientSession.prototype.disconnect = function(error) {
618 var state = (error == remoting.Error.NONE) ?
619 remoting.ClientSession.State.CLOSED :
620 remoting.ClientSession.State.FAILED;
622 // The plugin won't send a state change notification, so we explicitly log
623 // the fact that the connection has closed.
624 this.logToServer.logClientSessionStateChange(state, error, this.mode_);
626 this.setState_(state);
630 * Deletes the <embed> element from the container and disconnects.
632 * @return {void} Nothing.
634 remoting.ClientSession.prototype.cleanup = function() {
635 remoting.wcsSandbox.setOnIq(null);
638 'to="' + this.hostJid_ + '" ' +
640 'id="session-terminate" ' +
641 'xmlns:cli="jabber:client">' +
643 'xmlns="urn:xmpp:jingle:1" ' +
644 'action="session-terminate" ' +
645 'sid="' + this.sessionId_ + '">' +
646 '<reason><success/></reason>' +
653 * @return {remoting.ClientSession.Mode} The current state.
655 remoting.ClientSession.prototype.getMode = function() {
660 * @return {remoting.ClientSession.State} The current state.
662 remoting.ClientSession.prototype.getState = function() {
667 * @return {remoting.Error} The current error code.
669 remoting.ClientSession.prototype.getError = function() {
674 * Sends a key combination to the remoting client, by sending down events for
675 * the given keys, followed by up events in reverse order.
678 * @param {[number]} keys Key codes to be sent.
679 * @return {void} Nothing.
681 remoting.ClientSession.prototype.sendKeyCombination_ = function(keys) {
682 for (var i = 0; i < keys.length; i++) {
683 this.plugin_.injectKeyEvent(keys[i], true);
685 for (var i = 0; i < keys.length; i++) {
686 this.plugin_.injectKeyEvent(keys[i], false);
691 * Sends a Ctrl-Alt-Del sequence to the remoting client.
693 * @return {void} Nothing.
695 remoting.ClientSession.prototype.sendCtrlAltDel = function() {
696 this.sendKeyCombination_([0x0700e0, 0x0700e2, 0x07004c]);
700 * Sends a Print Screen keypress to the remoting client.
702 * @return {void} Nothing.
704 remoting.ClientSession.prototype.sendPrintScreen = function() {
705 this.sendKeyCombination_([0x070046]);
709 * Sets and stores the key remapping setting for the current host.
711 * @param {string} remappings Comma separated list of key remappings.
713 remoting.ClientSession.prototype.setRemapKeys = function(remappings) {
714 // Cancel any existing remappings and apply the new ones.
715 this.applyRemapKeys_(false);
716 this.remapKeys_ = remappings;
717 this.applyRemapKeys_(true);
719 // Save the new remapping setting.
721 options[remoting.ClientSession.KEY_REMAP_KEYS] = this.remapKeys_;
722 remoting.HostSettings.save(this.hostId_, options);
726 * Applies the configured key remappings to the session, or resets them.
728 * @param {boolean} apply True to apply remappings, false to cancel them.
730 remoting.ClientSession.prototype.applyRemapKeys_ = function(apply) {
731 // By default, under ChromeOS, remap the right Control key to the right
733 var remapKeys = this.remapKeys_;
734 if (remapKeys == '' && remoting.runningOnChromeOS()) {
735 remapKeys = '0x0700e4>0x0700e7';
738 if (remapKeys == '') {
742 var remappings = remapKeys.split(',');
743 for (var i = 0; i < remappings.length; ++i) {
744 var keyCodes = remappings[i].split('>');
745 if (keyCodes.length != 2) {
746 console.log('bad remapKey: ' + remappings[i]);
749 var fromKey = parseInt(keyCodes[0], 0);
750 var toKey = parseInt(keyCodes[1], 0);
751 if (!fromKey || !toKey) {
752 console.log('bad remapKey code: ' + remappings[i]);
756 console.log('remapKey 0x' + fromKey.toString(16) +
757 '>0x' + toKey.toString(16));
758 this.plugin_.remapKey(fromKey, toKey);
760 console.log('cancel remapKey 0x' + fromKey.toString(16));
761 this.plugin_.remapKey(fromKey, fromKey);
767 * Callback for the two "screen mode" related menu items: Resize desktop to
768 * fit and Shrink to fit.
770 * @param {Event} event The click event indicating which mode was selected.
771 * @return {void} Nothing.
774 remoting.ClientSession.prototype.onSetScreenMode_ = function(event) {
775 var shrinkToFit = this.shrinkToFit_;
776 var resizeToClient = this.resizeToClient_;
777 if (event.target == this.shrinkToFitButton_) {
778 shrinkToFit = !shrinkToFit;
780 if (event.target == this.resizeToClientButton_) {
781 resizeToClient = !resizeToClient;
783 this.setScreenMode_(shrinkToFit, resizeToClient);
787 * Set the shrink-to-fit and resize-to-client flags and save them if this is
788 * a Me2Me connection.
790 * @param {boolean} shrinkToFit True if the remote desktop should be scaled
791 * down if it is larger than the client window; false if scroll-bars
792 * should be added in this case.
793 * @param {boolean} resizeToClient True if window resizes should cause the
794 * host to attempt to resize its desktop to match the client window size;
795 * false to disable this behaviour for subsequent window resizes--the
796 * current host desktop size is not restored in this case.
797 * @return {void} Nothing.
800 remoting.ClientSession.prototype.setScreenMode_ =
801 function(shrinkToFit, resizeToClient) {
802 if (resizeToClient && !this.resizeToClient_) {
803 var clientArea = this.getClientArea_();
804 this.plugin_.notifyClientResolution(clientArea.width,
806 window.devicePixelRatio);
809 // If enabling shrink, reset bump-scroll offsets.
810 var needsScrollReset = shrinkToFit && !this.shrinkToFit_;
812 this.shrinkToFit_ = shrinkToFit;
813 this.resizeToClient_ = resizeToClient;
814 this.updateScrollbarVisibility();
816 if (this.hostId_ != '') {
818 options[remoting.ClientSession.KEY_SHRINK_TO_FIT] = this.shrinkToFit_;
819 options[remoting.ClientSession.KEY_RESIZE_TO_CLIENT] = this.resizeToClient_;
820 remoting.HostSettings.save(this.hostId_, options);
823 this.updateDimensions();
824 if (needsScrollReset) {
831 * Called when the client receives its first frame.
833 * @return {void} Nothing.
835 remoting.ClientSession.prototype.onFirstFrameReceived = function() {
836 this.hasReceivedFrame_ = true;
840 * @return {boolean} Whether the client has received a video buffer.
842 remoting.ClientSession.prototype.hasReceivedFrame = function() {
843 return this.hasReceivedFrame_;
847 * Sends an IQ stanza via the http xmpp proxy.
850 * @param {string} msg XML string of IQ stanza to send to server.
851 * @return {void} Nothing.
853 remoting.ClientSession.prototype.sendIq_ = function(msg) {
854 // Extract the session id, so we can close the session later.
855 var parser = new DOMParser();
856 var iqNode = parser.parseFromString(msg, 'text/xml').firstChild;
857 var jingleNode = iqNode.firstChild;
859 var action = jingleNode.getAttribute('action');
860 if (jingleNode.nodeName == 'jingle' && action == 'session-initiate') {
861 this.sessionId_ = jingleNode.getAttribute('sid');
865 // HACK: Add 'x' prefix to the IDs of the outgoing messages to make sure that
866 // stanza IDs used by host and client do not match. This is necessary to
867 // workaround bug in the signaling endpoint used by chromoting.
868 // TODO(sergeyu): Remove this hack once the server-side bug is fixed.
869 var type = iqNode.getAttribute('type');
871 var id = iqNode.getAttribute('id');
872 iqNode.setAttribute('id', 'x' + id);
873 msg = (new XMLSerializer()).serializeToString(iqNode);
876 console.log(remoting.timestamp(), remoting.formatIq.prettifySendIq(msg));
879 remoting.wcsSandbox.sendIq(msg);
882 remoting.ClientSession.prototype.initiateConnection_ = function() {
883 /** @type {remoting.ClientSession} */
886 remoting.wcsSandbox.connect(onWcsConnected, this.resetWithError_.bind(this));
888 /** @param {string} localJid Local JID. */
889 function onWcsConnected(localJid) {
890 that.connectPluginToWcs_(localJid);
891 that.getSharedSecret_(onSharedSecretReceived.bind(null, localJid));
894 /** @param {string} localJid Local JID.
895 * @param {string} sharedSecret Shared secret. */
896 function onSharedSecretReceived(localJid, sharedSecret) {
897 that.plugin_.connect(
898 that.hostJid_, that.hostPublicKey_, localJid, sharedSecret,
899 that.authenticationMethods_, that.hostId_, that.clientPairingId_,
900 that.clientPairedSecret_);
905 * Connects the plugin to WCS.
908 * @param {string} localJid Local JID.
909 * @return {void} Nothing.
911 remoting.ClientSession.prototype.connectPluginToWcs_ = function(localJid) {
912 remoting.formatIq.setJids(localJid, this.hostJid_);
913 var forwardIq = this.plugin_.onIncomingIq.bind(this.plugin_);
914 /** @param {string} stanza The IQ stanza received. */
915 var onIncomingIq = function(stanza) {
916 // HACK: Remove 'x' prefix added to the id in sendIq_().
918 var parser = new DOMParser();
919 var iqNode = parser.parseFromString(stanza, 'text/xml').firstChild;
920 var type = iqNode.getAttribute('type');
921 var id = iqNode.getAttribute('id');
922 if (type != 'set' && id.charAt(0) == 'x') {
923 iqNode.setAttribute('id', id.substr(1));
924 stanza = (new XMLSerializer()).serializeToString(iqNode);
927 // Pass message as is when it is malformed.
930 console.log(remoting.timestamp(),
931 remoting.formatIq.prettifyReceiveIq(stanza));
934 remoting.wcsSandbox.setOnIq(onIncomingIq);
938 * Gets shared secret to be used for connection.
940 * @param {function(string)} callback Callback called with the shared secret.
941 * @return {void} Nothing.
944 remoting.ClientSession.prototype.getSharedSecret_ = function(callback) {
945 /** @type remoting.ClientSession */
947 if (this.plugin_.hasFeature(remoting.ClientPlugin.Feature.THIRD_PARTY_AUTH)) {
948 /** @type{function(string, string, string): void} */
949 var fetchThirdPartyToken = function(tokenUrl, hostPublicKey, scope) {
950 that.fetchThirdPartyToken_(
951 tokenUrl, hostPublicKey, scope,
952 that.plugin_.onThirdPartyTokenFetched.bind(that.plugin_));
954 this.plugin_.fetchThirdPartyTokenHandler = fetchThirdPartyToken;
956 if (this.accessCode_) {
957 // Shared secret was already supplied before connecting (It2Me case).
958 callback(this.accessCode_);
959 } else if (this.plugin_.hasFeature(
960 remoting.ClientPlugin.Feature.ASYNC_PIN)) {
961 // Plugin supports asynchronously asking for the PIN.
962 this.plugin_.useAsyncPinDialog();
963 /** @param {boolean} pairingSupported */
964 var fetchPin = function(pairingSupported) {
965 that.fetchPin_(pairingSupported,
966 that.plugin_.onPinFetched.bind(that.plugin_));
968 this.plugin_.fetchPinHandler = fetchPin;
971 // Clients that don't support asking for a PIN asynchronously also don't
972 // support pairing, so request the PIN now without offering to remember it.
973 this.fetchPin_(false, callback);
978 * Callback that the plugin invokes to indicate that the connection
979 * status has changed.
982 * @param {number} status The plugin's status.
983 * @param {number} error The plugin's error state, if any.
985 remoting.ClientSession.prototype.onConnectionStatusUpdate_ =
986 function(status, error) {
987 if (status == remoting.ClientSession.State.CONNECTED) {
988 this.setFocusHandlers_();
989 this.onDesktopSizeChanged_();
990 if (this.resizeToClient_) {
991 var clientArea = this.getClientArea_();
992 this.plugin_.notifyClientResolution(clientArea.width,
994 window.devicePixelRatio);
996 // Activate full-screen related UX.
997 remoting.fullscreen.addListener(this.callOnFullScreenChanged_);
998 remoting.fullscreen.syncWithMaximize(true);
999 if (remoting.windowFrame) {
1000 remoting.windowFrame.setConnected(true);
1003 this.container_.addEventListener('mousemove',
1004 this.updateMouseCursorPosition_,
1007 } else if (status == remoting.ClientSession.State.FAILED) {
1009 case remoting.ClientSession.ConnectionError.HOST_IS_OFFLINE:
1010 this.error_ = remoting.Error.HOST_IS_OFFLINE;
1012 case remoting.ClientSession.ConnectionError.SESSION_REJECTED:
1013 this.error_ = remoting.Error.INVALID_ACCESS_CODE;
1015 case remoting.ClientSession.ConnectionError.INCOMPATIBLE_PROTOCOL:
1016 this.error_ = remoting.Error.INCOMPATIBLE_PROTOCOL;
1018 case remoting.ClientSession.ConnectionError.NETWORK_FAILURE:
1019 this.error_ = remoting.Error.P2P_FAILURE;
1021 case remoting.ClientSession.ConnectionError.HOST_OVERLOAD:
1022 this.error_ = remoting.Error.HOST_OVERLOAD;
1025 this.error_ = remoting.Error.UNEXPECTED;
1028 this.setState_(/** @type {remoting.ClientSession.State} */ (status));
1032 * Callback that the plugin invokes to indicate when the connection is
1036 * @param {boolean} ready True if the connection is ready.
1038 remoting.ClientSession.prototype.onConnectionReady_ = function(ready) {
1039 var container = /** @type {HTMLMediaElement} */(
1040 document.getElementById('video-container'));
1042 container.classList.add('session-client-inactive');
1044 container.classList.remove('session-client-inactive');
1047 this.raiseEvent(remoting.ClientSession.Events.videoChannelStateChanged,
1052 * Called when the client-host capabilities negotiation is complete.
1054 * @param {!Array.<string>} capabilities The set of capabilities negotiated
1055 * between the client and host.
1056 * @return {void} Nothing.
1059 remoting.ClientSession.prototype.onSetCapabilities_ = function(capabilities) {
1060 if (this.capabilities_ != null) {
1061 console.error('onSetCapabilities_() is called more than once');
1065 this.capabilities_ = capabilities;
1066 if (this.hasCapability_(
1067 remoting.ClientSession.Capability.SEND_INITIAL_RESOLUTION)) {
1068 var clientArea = this.getClientArea_();
1069 this.plugin_.notifyClientResolution(clientArea.width,
1071 window.devicePixelRatio);
1077 * @param {remoting.ClientSession.State} newState The new state for the session.
1078 * @return {void} Nothing.
1080 remoting.ClientSession.prototype.setState_ = function(newState) {
1081 var oldState = this.state_;
1082 this.state_ = newState;
1083 var state = this.state_;
1084 if (oldState == remoting.ClientSession.State.CONNECTING) {
1085 if (this.state_ == remoting.ClientSession.State.CLOSED) {
1086 state = remoting.ClientSession.State.CONNECTION_CANCELED;
1087 } else if (this.state_ == remoting.ClientSession.State.FAILED &&
1088 this.error_ == remoting.Error.HOST_IS_OFFLINE &&
1089 !this.logHostOfflineErrors_) {
1090 // The application requested host-offline errors to be suppressed, for
1091 // example, because this connection attempt is using a cached host JID.
1092 console.log('Suppressing host-offline error.');
1093 state = remoting.ClientSession.State.CONNECTION_CANCELED;
1095 } else if (oldState == remoting.ClientSession.State.CONNECTED &&
1096 this.state_ == remoting.ClientSession.State.FAILED) {
1097 state = remoting.ClientSession.State.CONNECTION_DROPPED;
1099 this.logToServer.logClientSessionStateChange(state, this.error_, this.mode_);
1100 if (this.state_ == remoting.ClientSession.State.CONNECTED) {
1101 this.createGnubbyAuthHandler_();
1104 this.raiseEvent(remoting.ClientSession.Events.stateChanged,
1105 new remoting.ClientSession.StateEvent(newState, oldState)
1110 * This is a callback that gets called when the window is resized.
1112 * @return {void} Nothing.
1114 remoting.ClientSession.prototype.onResize = function() {
1115 this.updateDimensions();
1117 if (this.notifyClientResolutionTimer_) {
1118 window.clearTimeout(this.notifyClientResolutionTimer_);
1119 this.notifyClientResolutionTimer_ = null;
1122 // Defer notifying the host of the change until the window stops resizing, to
1123 // avoid overloading the control channel with notifications.
1124 if (this.resizeToClient_) {
1125 var kResizeRateLimitMs = 1000;
1126 if (this.hasCapability_(
1127 remoting.ClientSession.Capability.RATE_LIMIT_RESIZE_REQUESTS)) {
1128 kResizeRateLimitMs = 250;
1130 var clientArea = this.getClientArea_();
1131 this.notifyClientResolutionTimer_ = window.setTimeout(
1132 this.plugin_.notifyClientResolution.bind(this.plugin_,
1135 window.devicePixelRatio),
1136 kResizeRateLimitMs);
1139 // If bump-scrolling is enabled, adjust the plugin margins to fully utilize
1140 // the new window area.
1141 this.resetScroll_();
1143 this.updateScrollbarVisibility();
1147 * Requests that the host pause or resume video updates.
1149 * @param {boolean} pause True to pause video, false to resume.
1150 * @return {void} Nothing.
1152 remoting.ClientSession.prototype.pauseVideo = function(pause) {
1154 this.plugin_.pauseVideo(pause);
1159 * Requests that the host pause or resume audio.
1161 * @param {boolean} pause True to pause audio, false to resume.
1162 * @return {void} Nothing.
1164 remoting.ClientSession.prototype.pauseAudio = function(pause) {
1166 this.plugin_.pauseAudio(pause)
1171 * This is a callback that gets called when the plugin notifies us of a change
1172 * in the size of the remote desktop.
1175 * @return {void} Nothing.
1177 remoting.ClientSession.prototype.onDesktopSizeChanged_ = function() {
1178 console.log('desktop size changed: ' +
1179 this.plugin_.desktopWidth + 'x' +
1180 this.plugin_.desktopHeight +' @ ' +
1181 this.plugin_.desktopXDpi + 'x' +
1182 this.plugin_.desktopYDpi + ' DPI');
1183 this.updateDimensions();
1184 this.updateScrollbarVisibility();
1188 * Refreshes the plugin's dimensions, taking into account the sizes of the
1189 * remote desktop and client window, and the current scale-to-fit setting.
1191 * @return {void} Nothing.
1193 remoting.ClientSession.prototype.updateDimensions = function() {
1194 if (this.plugin_.desktopWidth == 0 ||
1195 this.plugin_.desktopHeight == 0) {
1199 var clientArea = this.getClientArea_();
1200 var desktopWidth = this.plugin_.desktopWidth;
1201 var desktopHeight = this.plugin_.desktopHeight;
1203 // When configured to display a host at its original size, we aim to display
1204 // it as close to its physical size as possible, without losing data:
1205 // - If client and host have matching DPI, render the host pixel-for-pixel.
1206 // - If the host has higher DPI then still render pixel-for-pixel.
1207 // - If the host has lower DPI then let Chrome up-scale it to natural size.
1209 // We specify the plugin dimensions in Density-Independent Pixels, so to
1210 // render pixel-for-pixel we need to down-scale the host dimensions by the
1211 // devicePixelRatio of the client. To match the host pixel density, we choose
1212 // an initial scale factor based on the client devicePixelRatio and host DPI.
1214 // Determine the effective device pixel ratio of the host, based on DPI.
1215 var hostPixelRatioX = Math.ceil(this.plugin_.desktopXDpi / 96);
1216 var hostPixelRatioY = Math.ceil(this.plugin_.desktopYDpi / 96);
1217 var hostPixelRatio = Math.min(hostPixelRatioX, hostPixelRatioY);
1219 // Down-scale by the smaller of the client and host ratios.
1220 var scale = 1.0 / Math.min(window.devicePixelRatio, hostPixelRatio);
1222 if (this.shrinkToFit_) {
1223 // Reduce the scale, if necessary, to fit the whole desktop in the window.
1224 var scaleFitWidth = Math.min(scale, 1.0 * clientArea.width / desktopWidth);
1225 var scaleFitHeight =
1226 Math.min(scale, 1.0 * clientArea.height / desktopHeight);
1227 scale = Math.min(scaleFitHeight, scaleFitWidth);
1229 // If we're running full-screen then try to handle common side-by-side
1230 // multi-monitor combinations more intelligently.
1231 if (remoting.fullscreen.isActive()) {
1232 // If the host has two monitors each the same size as the client then
1233 // scale-to-fit will have the desktop occupy only 50% of the client area,
1234 // in which case it would be preferable to down-scale less and let the
1235 // user bump-scroll around ("scale-and-pan").
1236 // Triggering scale-and-pan if less than 65% of the client area would be
1237 // used adds enough fuzz to cope with e.g. 1280x800 client connecting to
1238 // a (2x1280)x1024 host nicely.
1239 // Note that we don't need to account for scrollbars while fullscreen.
1240 if (scale <= scaleFitHeight * 0.65) {
1241 scale = scaleFitHeight;
1243 if (scale <= scaleFitWidth * 0.65) {
1244 scale = scaleFitWidth;
1249 var pluginWidth = Math.round(desktopWidth * scale);
1250 var pluginHeight = Math.round(desktopHeight * scale);
1253 this.video_.style.width = pluginWidth + 'px';
1254 this.video_.style.height = pluginHeight + 'px';
1257 // Resize the plugin if necessary.
1258 // TODO(wez): Handle high-DPI to high-DPI properly (crbug.com/135089).
1259 this.plugin_.element().style.width = pluginWidth + 'px';
1260 this.plugin_.element().style.height = pluginHeight + 'px';
1262 // Position the container.
1263 // Note that clientWidth/Height take into account scrollbars.
1264 var clientWidth = document.documentElement.clientWidth;
1265 var clientHeight = document.documentElement.clientHeight;
1266 var parentNode = this.plugin_.element().parentNode;
1268 console.log('plugin dimensions: ' +
1269 parentNode.style.left + ',' +
1270 parentNode.style.top + '-' +
1271 pluginWidth + 'x' + pluginHeight + '.');
1275 * Returns an associative array with a set of stats for this connection.
1277 * @return {remoting.ClientSession.PerfStats} The connection statistics.
1279 remoting.ClientSession.prototype.getPerfStats = function() {
1280 return this.plugin_.getPerfStats();
1286 * @param {remoting.ClientSession.PerfStats} stats
1288 remoting.ClientSession.prototype.logStatistics = function(stats) {
1289 this.logToServer.logStatistics(stats, this.mode_);
1293 * Enable or disable logging of connection errors due to a host being offline.
1294 * For example, if attempting a connection using a cached JID, host-offline
1295 * errors should not be logged because the JID will be refreshed and the
1296 * connection retried.
1298 * @param {boolean} enable True to log host-offline errors; false to suppress.
1300 remoting.ClientSession.prototype.logHostOfflineErrors = function(enable) {
1301 this.logHostOfflineErrors_ = enable;
1305 * Request pairing with the host for PIN-less authentication.
1307 * @param {string} clientName The human-readable name of the client.
1308 * @param {function(string, string):void} onDone Callback to receive the
1309 * client id and shared secret when they are available.
1311 remoting.ClientSession.prototype.requestPairing = function(clientName, onDone) {
1313 this.plugin_.requestPairing(clientName, onDone);
1318 * Called when the full-screen status has changed, either via the
1319 * remoting.Fullscreen class, or via a system event such as the Escape key
1321 * @param {boolean} fullscreen True if the app is entering full-screen mode;
1322 * false if it is leaving it.
1325 remoting.ClientSession.prototype.onFullScreenChanged_ = function (fullscreen) {
1326 var htmlNode = /** @type {HTMLElement} */ (document.documentElement);
1327 this.enableBumpScroll_(fullscreen);
1329 htmlNode.classList.add('full-screen');
1331 htmlNode.classList.remove('full-screen');
1336 * Updates the options menu to reflect the current scale-to-fit and full-screen
1338 * @return {void} Nothing.
1341 remoting.ClientSession.prototype.onShowOptionsMenu_ = function() {
1342 remoting.MenuButton.select(this.resizeToClientButton_, this.resizeToClient_);
1343 remoting.MenuButton.select(this.shrinkToFitButton_, this.shrinkToFit_);
1344 remoting.MenuButton.select(this.fullScreenButton_,
1345 remoting.fullscreen.isActive());
1349 * Scroll the client plugin by the specified amount, keeping it visible.
1350 * Note that this is only used in content full-screen mode (not windowed or
1351 * browser full-screen modes), where window.scrollBy and the scrollTop and
1352 * scrollLeft properties don't work.
1353 * @param {number} dx The amount by which to scroll horizontally. Positive to
1354 * scroll right; negative to scroll left.
1355 * @param {number} dy The amount by which to scroll vertically. Positive to
1356 * scroll down; negative to scroll up.
1357 * @return {boolean} True if the requested scroll had no effect because both
1358 * vertical and horizontal edges of the screen have been reached.
1361 remoting.ClientSession.prototype.scroll_ = function(dx, dy) {
1362 var plugin = this.plugin_.element();
1363 var style = plugin.style;
1366 * Helper function for x- and y-scrolling
1367 * @param {number|string} curr The current margin, eg. "10px".
1368 * @param {number} delta The requested scroll amount.
1369 * @param {number} windowBound The size of the window, in pixels.
1370 * @param {number} pluginBound The size of the plugin, in pixels.
1371 * @param {{stop: boolean}} stop Reference parameter used to indicate when
1372 * the scroll has reached one of the edges and can be stopped in that
1374 * @return {string} The new margin value.
1376 var adjustMargin = function(curr, delta, windowBound, pluginBound, stop) {
1377 var minMargin = Math.min(0, windowBound - pluginBound);
1378 var result = (curr ? parseFloat(curr) : 0) - delta;
1379 result = Math.min(0, Math.max(minMargin, result));
1380 stop.stop = (result == 0 || result == minMargin);
1381 return result + 'px';
1384 var stopX = { stop: false };
1385 var clientArea = this.getClientArea_();
1386 style.marginLeft = adjustMargin(style.marginLeft, dx,
1387 clientArea.width, plugin.clientWidth, stopX);
1389 var stopY = { stop: false };
1390 style.marginTop = adjustMargin(
1391 style.marginTop, dy, clientArea.height, plugin.clientHeight, stopY);
1392 return stopX.stop && stopY.stop;
1395 remoting.ClientSession.prototype.resetScroll_ = function() {
1397 var plugin = this.plugin_.element();
1398 plugin.style.marginTop = '0px';
1399 plugin.style.marginLeft = '0px';
1404 * Enable or disable bump-scrolling. When disabling bump scrolling, also reset
1405 * the scroll offsets to (0, 0).
1407 * @param {boolean} enable True to enable bump-scrolling, false to disable it.
1409 remoting.ClientSession.prototype.enableBumpScroll_ = function(enable) {
1410 var element = /*@type{HTMLElement} */ document.documentElement;
1412 /** @type {null|function(Event):void} */
1413 this.onMouseMoveRef_ = this.onMouseMove_.bind(this);
1414 element.addEventListener('mousemove', this.onMouseMoveRef_, false);
1416 element.removeEventListener('mousemove', this.onMouseMoveRef_, false);
1417 this.onMouseMoveRef_ = null;
1418 this.resetScroll_();
1423 * @param {Event} event The mouse event.
1426 remoting.ClientSession.prototype.onMouseMove_ = function(event) {
1427 if (this.bumpScrollTimer_) {
1428 window.clearTimeout(this.bumpScrollTimer_);
1429 this.bumpScrollTimer_ = null;
1433 * Compute the scroll speed based on how close the mouse is to the edge.
1434 * @param {number} mousePos The mouse x- or y-coordinate
1435 * @param {number} size The width or height of the content area.
1436 * @return {number} The scroll delta, in pixels.
1438 var computeDelta = function(mousePos, size) {
1440 if (mousePos >= size - threshold) {
1441 return 1 + 5 * (mousePos - (size - threshold)) / threshold;
1442 } else if (mousePos <= threshold) {
1443 return -1 - 5 * (threshold - mousePos) / threshold;
1448 var clientArea = this.getClientArea_();
1449 var dx = computeDelta(event.x, clientArea.width);
1450 var dy = computeDelta(event.y, clientArea.height);
1452 if (dx != 0 || dy != 0) {
1453 /** @type {remoting.ClientSession} */
1456 * Scroll the view, and schedule a timer to do so again unless we've hit
1457 * the edges of the screen. This timer is cancelled when the mouse moves.
1458 * @param {number} expected The time at which we expect to be called.
1460 var repeatScroll = function(expected) {
1461 /** @type {number} */
1462 var now = new Date().getTime();
1463 /** @type {number} */
1465 var lateAdjustment = 1 + (now - expected) / timeout;
1466 if (!that.scroll_(lateAdjustment * dx, lateAdjustment * dy)) {
1467 that.bumpScrollTimer_ = window.setTimeout(
1468 function() { repeatScroll(now + timeout); },
1472 repeatScroll(new Date().getTime());
1477 * Sends a clipboard item to the host.
1479 * @param {string} mimeType The MIME type of the clipboard item.
1480 * @param {string} item The clipboard item.
1482 remoting.ClientSession.prototype.sendClipboardItem = function(mimeType, item) {
1485 this.plugin_.sendClipboardItem(mimeType, item);
1489 * Send a gnubby-auth extension message to the host.
1490 * @param {Object} data The gnubby-auth message data.
1492 remoting.ClientSession.prototype.sendGnubbyAuthMessage = function(data) {
1495 this.plugin_.sendClientMessage('gnubby-auth', JSON.stringify(data));
1499 * Process a remote gnubby auth request.
1500 * @param {string} data Remote gnubby request data.
1503 remoting.ClientSession.prototype.processGnubbyAuthMessage_ = function(data) {
1504 if (this.gnubbyAuthHandler_) {
1506 this.gnubbyAuthHandler_.onMessage(data);
1508 console.error('Failed to process gnubby message: ',
1509 /** @type {*} */ (err));
1512 console.error('Received unexpected gnubby message');
1517 * Create a gnubby auth handler and inform the host that gnubby auth is
1521 remoting.ClientSession.prototype.createGnubbyAuthHandler_ = function() {
1522 if (this.mode_ == remoting.ClientSession.Mode.ME2ME) {
1523 this.gnubbyAuthHandler_ = new remoting.GnubbyAuthHandler(this);
1524 // TODO(psj): Move to more generic capabilities mechanism.
1525 this.sendGnubbyAuthMessage({'type': 'control', 'option': 'auth-v1'});
1530 * @return {{width: number, height: number}} The height of the window's client
1531 * area. This differs between apps v1 and apps v2 due to the custom window
1532 * borders used by the latter.
1535 remoting.ClientSession.prototype.getClientArea_ = function() {
1536 return remoting.windowFrame ?
1537 remoting.windowFrame.getClientArea() :
1538 { 'width': window.innerWidth, 'height': window.innerHeight };
1542 * @param {string} url
1543 * @param {number} hotspotX
1544 * @param {number} hotspotY
1546 remoting.ClientSession.prototype.updateMouseCursorImage_ =
1547 function(url, hotspotX, hotspotY) {
1548 this.mouseCursorOverlay_.hidden = !url;
1550 this.mouseCursorOverlay_.style.marginLeft = '-' + hotspotX + 'px';
1551 this.mouseCursorOverlay_.style.marginTop = '-' + hotspotY + 'px';
1552 this.mouseCursorOverlay_.src = url;