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.
48 remoting.ClientSession = function(accessCode, fetchPin, fetchThirdPartyToken,
49 authenticationMethods,
50 hostId, hostJid, hostPublicKey, mode,
51 clientPairingId, clientPairedSecret) {
53 this.state_ = remoting.ClientSession.State.CREATED;
56 this.error_ = remoting.Error.NONE;
59 this.hostJid_ = hostJid;
61 this.hostPublicKey_ = hostPublicKey;
63 this.accessCode_ = accessCode;
65 this.fetchPin_ = fetchPin;
67 this.fetchThirdPartyToken_ = fetchThirdPartyToken;
69 this.authenticationMethods_ = authenticationMethods;
71 this.hostId_ = hostId;
75 this.clientPairingId_ = clientPairingId;
77 this.clientPairedSecret_ = clientPairedSecret;
80 /** @type {remoting.ClientPlugin}
84 this.shrinkToFit_ = true;
86 this.resizeToClient_ = true;
90 this.hasReceivedFrame_ = false;
91 this.logToServer = new remoting.LogToServer();
92 /** @type {?function(remoting.ClientSession.State,
93 remoting.ClientSession.State):void} */
94 this.onStateChange_ = null;
96 /** @type {number?} @private */
97 this.notifyClientResolutionTimer_ = null;
98 /** @type {number?} @private */
99 this.bumpScrollTimer_ = null;
102 * Allow host-offline error reporting to be suppressed in situations where it
103 * would not be useful, for example, when using a cached host JID.
105 * @type {boolean} @private
107 this.logHostOfflineErrors_ = true;
110 this.callPluginLostFocus_ = this.pluginLostFocus_.bind(this);
112 this.callPluginGotFocus_ = this.pluginGotFocus_.bind(this);
114 this.callSetScreenMode_ = this.onSetScreenMode_.bind(this);
116 this.callToggleFullScreen_ = this.toggleFullScreen_.bind(this);
119 this.screenOptionsMenu_ = new remoting.MenuButton(
120 document.getElementById('screen-options-menu'),
121 this.onShowOptionsMenu_.bind(this));
123 this.sendKeysMenu_ = new remoting.MenuButton(
124 document.getElementById('send-keys-menu')
127 /** @type {HTMLElement} @private */
128 this.resizeToClientButton_ =
129 document.getElementById('screen-resize-to-client');
130 /** @type {HTMLElement} @private */
131 this.shrinkToFitButton_ = document.getElementById('screen-shrink-to-fit');
132 /** @type {HTMLElement} @private */
133 this.fullScreenButton_ = document.getElementById('toggle-full-screen');
135 if (this.mode_ == remoting.ClientSession.Mode.IT2ME) {
136 // Resize-to-client is not supported for IT2Me hosts.
137 this.resizeToClientButton_.hidden = true;
139 this.resizeToClientButton_.hidden = false;
140 this.resizeToClientButton_.addEventListener(
141 'click', this.callSetScreenMode_, false);
144 this.shrinkToFitButton_.addEventListener(
145 'click', this.callSetScreenMode_, false);
146 this.fullScreenButton_.addEventListener(
147 'click', this.callToggleFullScreen_, false);
151 * @param {?function(remoting.ClientSession.State,
152 remoting.ClientSession.State):void} onStateChange
153 * The callback to invoke when the session changes state.
155 remoting.ClientSession.prototype.setOnStateChange = function(onStateChange) {
156 this.onStateChange_ = onStateChange;
160 * Called when the window or desktop size or the scaling settings change,
161 * to set the scroll-bar visibility.
163 * TODO(jamiewalch): crbug.com/252796: Remove this once crbug.com/240772 is
166 remoting.ClientSession.prototype.updateScrollbarVisibility = function() {
167 var needsVerticalScroll = false;
168 var needsHorizontalScroll = false;
169 if (!this.shrinkToFit_) {
170 // Determine whether or not horizontal or vertical scrollbars are
171 // required, taking into account their width.
172 needsVerticalScroll = window.innerHeight < this.plugin_.desktopHeight;
173 needsHorizontalScroll = window.innerWidth < this.plugin_.desktopWidth;
174 var kScrollBarWidth = 16;
175 if (needsHorizontalScroll && !needsVerticalScroll) {
176 needsVerticalScroll =
177 window.innerHeight - kScrollBarWidth < this.plugin_.desktopHeight;
178 } else if (!needsHorizontalScroll && needsVerticalScroll) {
179 needsHorizontalScroll =
180 window.innerWidth - kScrollBarWidth < this.plugin_.desktopWidth;
184 var htmlNode = /** @type {HTMLElement} */ (document.body.parentNode);
185 if (needsHorizontalScroll) {
186 htmlNode.classList.remove('no-horizontal-scroll');
188 htmlNode.classList.add('no-horizontal-scroll');
190 if (needsVerticalScroll) {
191 htmlNode.classList.remove('no-vertical-scroll');
193 htmlNode.classList.add('no-vertical-scroll');
197 // Note that the positive values in both of these enums are copied directly
198 // from chromoting_scriptable_object.h and must be kept in sync. The negative
199 // values represent state transitions that occur within the web-app that have
200 // no corresponding plugin state transition.
201 /** @enum {number} */
202 remoting.ClientSession.State = {
203 CONNECTION_CANCELED: -3, // Connection closed (gracefully) before connecting.
204 CONNECTION_DROPPED: -2, // Succeeded, but subsequently closed with an error.
214 /** @enum {number} */
215 remoting.ClientSession.ConnectionError = {
220 INCOMPATIBLE_PROTOCOL: 3,
225 // The mode of this session.
226 /** @enum {number} */
227 remoting.ClientSession.Mode = {
233 * Type used for performance statistics collected by the plugin.
236 remoting.ClientSession.PerfStats = function() {};
237 /** @type {number} */
238 remoting.ClientSession.PerfStats.prototype.videoBandwidth;
239 /** @type {number} */
240 remoting.ClientSession.PerfStats.prototype.videoFrameRate;
241 /** @type {number} */
242 remoting.ClientSession.PerfStats.prototype.captureLatency;
243 /** @type {number} */
244 remoting.ClientSession.PerfStats.prototype.encodeLatency;
245 /** @type {number} */
246 remoting.ClientSession.PerfStats.prototype.decodeLatency;
247 /** @type {number} */
248 remoting.ClientSession.PerfStats.prototype.renderLatency;
249 /** @type {number} */
250 remoting.ClientSession.PerfStats.prototype.roundtripLatency;
252 // Keys for connection statistics.
253 remoting.ClientSession.STATS_KEY_VIDEO_BANDWIDTH = 'videoBandwidth';
254 remoting.ClientSession.STATS_KEY_VIDEO_FRAME_RATE = 'videoFrameRate';
255 remoting.ClientSession.STATS_KEY_CAPTURE_LATENCY = 'captureLatency';
256 remoting.ClientSession.STATS_KEY_ENCODE_LATENCY = 'encodeLatency';
257 remoting.ClientSession.STATS_KEY_DECODE_LATENCY = 'decodeLatency';
258 remoting.ClientSession.STATS_KEY_RENDER_LATENCY = 'renderLatency';
259 remoting.ClientSession.STATS_KEY_ROUNDTRIP_LATENCY = 'roundtripLatency';
261 // Keys for per-host settings.
262 remoting.ClientSession.KEY_REMAP_KEYS = 'remapKeys';
263 remoting.ClientSession.KEY_RESIZE_TO_CLIENT = 'resizeToClient';
264 remoting.ClientSession.KEY_SHRINK_TO_FIT = 'shrinkToFit';
267 * The id of the client plugin
271 remoting.ClientSession.prototype.PLUGIN_ID = 'session-client-plugin';
274 * Set of capabilities for which hasCapability_() can be used to test.
278 remoting.ClientSession.Capability = {
279 // When enabled this capability causes the client to send its screen
280 // resolution to the host once connection has been established. See
281 // this.plugin_.notifyClientResolution().
282 SEND_INITIAL_RESOLUTION: 'sendInitialResolution',
283 RATE_LIMIT_RESIZE_REQUESTS: 'rateLimitResizeRequests'
287 * The set of capabilities negotiated between the client and host.
288 * @type {Array.<string>}
291 remoting.ClientSession.prototype.capabilities_ = null;
294 * @param {remoting.ClientSession.Capability} capability The capability to test
296 * @return {boolean} True if the capability has been negotiated between
297 * the client and host.
300 remoting.ClientSession.prototype.hasCapability_ = function(capability) {
301 if (this.capabilities_ == null)
304 return this.capabilities_.indexOf(capability) > -1;
308 * @param {Element} container The element to add the plugin to.
309 * @param {string} id Id to use for the plugin element .
310 * @return {remoting.ClientPlugin} Create plugin object for the locally
313 remoting.ClientSession.prototype.createClientPlugin_ = function(container, id) {
314 var plugin = /** @type {remoting.ViewerPlugin} */
315 document.createElement('embed');
318 plugin.src = 'about://none';
319 plugin.type = 'application/vnd.chromium.remoting-viewer';
322 plugin.tabIndex = 0; // Required, otherwise focus() doesn't work.
323 container.appendChild(plugin);
325 return new remoting.ClientPlugin(plugin);
329 * Callback function called when the plugin element gets focus.
331 remoting.ClientSession.prototype.pluginGotFocus_ = function() {
332 remoting.clipboard.initiateToHost();
336 * Callback function called when the plugin element loses focus.
338 remoting.ClientSession.prototype.pluginLostFocus_ = function() {
340 // Release all keys to prevent them becoming 'stuck down' on the host.
341 this.plugin_.releaseAllKeys();
342 if (this.plugin_.element()) {
343 // Focus should stay on the element, not (for example) the toolbar.
344 this.plugin_.element().focus();
350 * Adds <embed> element to |container| and readies the sesion object.
352 * @param {Element} container The element to add the plugin to.
354 remoting.ClientSession.prototype.createPluginAndConnect =
355 function(container) {
356 this.plugin_ = this.createClientPlugin_(container, this.PLUGIN_ID);
357 remoting.HostSettings.load(this.hostId_,
358 this.onHostSettingsLoaded_.bind(this));
362 * @param {Object.<string>} options The current options for the host, or {}
363 * if this client has no saved settings for the host.
366 remoting.ClientSession.prototype.onHostSettingsLoaded_ = function(options) {
367 if (remoting.ClientSession.KEY_REMAP_KEYS in options &&
368 typeof(options[remoting.ClientSession.KEY_REMAP_KEYS]) ==
370 this.remapKeys_ = /** @type {string} */
371 options[remoting.ClientSession.KEY_REMAP_KEYS];
373 if (remoting.ClientSession.KEY_RESIZE_TO_CLIENT in options &&
374 typeof(options[remoting.ClientSession.KEY_RESIZE_TO_CLIENT]) ==
376 this.resizeToClient_ = /** @type {boolean} */
377 options[remoting.ClientSession.KEY_RESIZE_TO_CLIENT];
379 if (remoting.ClientSession.KEY_SHRINK_TO_FIT in options &&
380 typeof(options[remoting.ClientSession.KEY_SHRINK_TO_FIT]) ==
382 this.shrinkToFit_ = /** @type {boolean} */
383 options[remoting.ClientSession.KEY_SHRINK_TO_FIT];
386 /** @param {boolean} result */
387 this.plugin_.initialize(this.onPluginInitialized_.bind(this));
391 * Constrains the focus to the plugin element.
394 remoting.ClientSession.prototype.setFocusHandlers_ = function() {
395 this.plugin_.element().addEventListener(
396 'focus', this.callPluginGotFocus_, false);
397 this.plugin_.element().addEventListener(
398 'blur', this.callPluginLostFocus_, false);
399 this.plugin_.element().focus();
403 * @param {remoting.Error} error
405 remoting.ClientSession.prototype.resetWithError_ = function(error) {
406 this.plugin_.cleanup();
409 this.setState_(remoting.ClientSession.State.FAILED);
413 * @param {boolean} initialized
415 remoting.ClientSession.prototype.onPluginInitialized_ = function(initialized) {
417 console.error('ERROR: remoting plugin not loaded');
418 this.resetWithError_(remoting.Error.MISSING_PLUGIN);
422 if (!this.plugin_.isSupportedVersion()) {
423 this.resetWithError_(remoting.Error.BAD_PLUGIN_VERSION);
427 // Show the Send Keys menu only if the plugin has the injectKeyEvent feature,
428 // and the Ctrl-Alt-Del button only in Me2Me mode.
429 if (!this.plugin_.hasFeature(
430 remoting.ClientPlugin.Feature.INJECT_KEY_EVENT)) {
431 var sendKeysElement = document.getElementById('send-keys-menu');
432 sendKeysElement.hidden = true;
433 } else if (this.mode_ != remoting.ClientSession.Mode.ME2ME) {
434 var sendCadElement = document.getElementById('send-ctrl-alt-del');
435 sendCadElement.hidden = true;
438 // Apply customized key remappings if the plugin supports remapKeys.
439 if (this.plugin_.hasFeature(remoting.ClientPlugin.Feature.REMAP_KEY)) {
440 this.applyRemapKeys_(true);
443 /** @param {string} msg The IQ stanza to send. */
444 this.plugin_.onOutgoingIqHandler = this.sendIq_.bind(this);
445 /** @param {string} msg The message to log. */
446 this.plugin_.onDebugMessageHandler = function(msg) {
447 console.log('plugin: ' + msg);
450 this.plugin_.onConnectionStatusUpdateHandler =
451 this.onConnectionStatusUpdate_.bind(this);
452 this.plugin_.onConnectionReadyHandler =
453 this.onConnectionReady_.bind(this);
454 this.plugin_.onDesktopSizeUpdateHandler =
455 this.onDesktopSizeChanged_.bind(this);
456 this.plugin_.onSetCapabilitiesHandler =
457 this.onSetCapabilities_.bind(this);
458 this.initiateConnection_();
462 * Deletes the <embed> element from the container, without sending a
463 * session_terminate request. This is to be called when the session was
464 * disconnected by the Host.
466 * @return {void} Nothing.
468 remoting.ClientSession.prototype.removePlugin = function() {
470 this.plugin_.element().removeEventListener(
471 'focus', this.callPluginGotFocus_, false);
472 this.plugin_.element().removeEventListener(
473 'blur', this.callPluginLostFocus_, false);
474 this.plugin_.cleanup();
478 // Delete event handlers that aren't relevent when not connected.
479 this.resizeToClientButton_.removeEventListener(
480 'click', this.callSetScreenMode_, false);
481 this.shrinkToFitButton_.removeEventListener(
482 'click', this.callSetScreenMode_, false);
483 this.fullScreenButton_.removeEventListener(
484 'click', this.callToggleFullScreen_, false);
486 // In case the user had selected full-screen mode, cancel it now.
487 document.webkitCancelFullScreen();
491 * Deletes the <embed> element from the container and disconnects.
493 * @param {boolean} isUserInitiated True for user-initiated disconnects, False
494 * for disconnects due to connection failures.
495 * @return {void} Nothing.
497 remoting.ClientSession.prototype.disconnect = function(isUserInitiated) {
498 if (isUserInitiated) {
499 // The plugin won't send a state change notification, so we explicitly log
500 // the fact that the connection has closed.
501 this.logToServer.logClientSessionStateChange(
502 remoting.ClientSession.State.CLOSED, remoting.Error.NONE, this.mode_);
504 remoting.wcsSandbox.setOnIq(null);
507 'to="' + this.hostJid_ + '" ' +
509 'id="session-terminate" ' +
510 'xmlns:cli="jabber:client">' +
512 'xmlns="urn:xmpp:jingle:1" ' +
513 'action="session-terminate" ' +
514 'sid="' + this.sessionId_ + '">' +
515 '<reason><success/></reason>' +
522 * @return {remoting.ClientSession.Mode} The current state.
524 remoting.ClientSession.prototype.getMode = function() {
529 * @return {remoting.ClientSession.State} The current state.
531 remoting.ClientSession.prototype.getState = function() {
536 * @return {remoting.Error} The current error code.
538 remoting.ClientSession.prototype.getError = function() {
543 * Sends a key combination to the remoting client, by sending down events for
544 * the given keys, followed by up events in reverse order.
547 * @param {[number]} keys Key codes to be sent.
548 * @return {void} Nothing.
550 remoting.ClientSession.prototype.sendKeyCombination_ = function(keys) {
551 for (var i = 0; i < keys.length; i++) {
552 this.plugin_.injectKeyEvent(keys[i], true);
554 for (var i = 0; i < keys.length; i++) {
555 this.plugin_.injectKeyEvent(keys[i], false);
560 * Sends a Ctrl-Alt-Del sequence to the remoting client.
562 * @return {void} Nothing.
564 remoting.ClientSession.prototype.sendCtrlAltDel = function() {
565 this.sendKeyCombination_([0x0700e0, 0x0700e2, 0x07004c]);
569 * Sends a Print Screen keypress to the remoting client.
571 * @return {void} Nothing.
573 remoting.ClientSession.prototype.sendPrintScreen = function() {
574 this.sendKeyCombination_([0x070046]);
578 * Sets and stores the key remapping setting for the current host.
580 * @param {string} remappings Comma separated list of key remappings.
582 remoting.ClientSession.prototype.setRemapKeys = function(remappings) {
583 // Cancel any existing remappings and apply the new ones.
584 this.applyRemapKeys_(false);
585 this.remapKeys_ = remappings;
586 this.applyRemapKeys_(true);
588 // Save the new remapping setting.
590 options[remoting.ClientSession.KEY_REMAP_KEYS] = this.remapKeys_;
591 remoting.HostSettings.save(this.hostId_, options);
595 * Applies the configured key remappings to the session, or resets them.
597 * @param {boolean} apply True to apply remappings, false to cancel them.
599 remoting.ClientSession.prototype.applyRemapKeys_ = function(apply) {
600 // By default, under ChromeOS, remap the right Control key to the right
602 var remapKeys = this.remapKeys_;
603 if (remapKeys == '' && remoting.runningOnChromeOS()) {
604 remapKeys = '0x0700e4>0x0700e7';
607 var remappings = remapKeys.split(',');
608 for (var i = 0; i < remappings.length; ++i) {
609 var keyCodes = remappings[i].split('>');
610 if (keyCodes.length != 2) {
611 console.log('bad remapKey: ' + remappings[i]);
614 var fromKey = parseInt(keyCodes[0], 0);
615 var toKey = parseInt(keyCodes[1], 0);
616 if (!fromKey || !toKey) {
617 console.log('bad remapKey code: ' + remappings[i]);
621 console.log('remapKey 0x' + fromKey.toString(16) +
622 '>0x' + toKey.toString(16));
623 this.plugin_.remapKey(fromKey, toKey);
625 console.log('cancel remapKey 0x' + fromKey.toString(16));
626 this.plugin_.remapKey(fromKey, fromKey);
632 * Callback for the two "screen mode" related menu items: Resize desktop to
633 * fit and Shrink to fit.
635 * @param {Event} event The click event indicating which mode was selected.
636 * @return {void} Nothing.
639 remoting.ClientSession.prototype.onSetScreenMode_ = function(event) {
640 var shrinkToFit = this.shrinkToFit_;
641 var resizeToClient = this.resizeToClient_;
642 if (event.target == this.shrinkToFitButton_) {
643 shrinkToFit = !shrinkToFit;
645 if (event.target == this.resizeToClientButton_) {
646 resizeToClient = !resizeToClient;
648 this.setScreenMode_(shrinkToFit, resizeToClient);
652 * Set the shrink-to-fit and resize-to-client flags and save them if this is
653 * a Me2Me connection.
655 * @param {boolean} shrinkToFit True if the remote desktop should be scaled
656 * down if it is larger than the client window; false if scroll-bars
657 * should be added in this case.
658 * @param {boolean} resizeToClient True if window resizes should cause the
659 * host to attempt to resize its desktop to match the client window size;
660 * false to disable this behaviour for subsequent window resizes--the
661 * current host desktop size is not restored in this case.
662 * @return {void} Nothing.
665 remoting.ClientSession.prototype.setScreenMode_ =
666 function(shrinkToFit, resizeToClient) {
667 if (resizeToClient && !this.resizeToClient_) {
668 this.plugin_.notifyClientResolution(window.innerWidth,
670 window.devicePixelRatio);
673 // If enabling shrink, reset bump-scroll offsets.
674 var needsScrollReset = shrinkToFit && !this.shrinkToFit_;
676 this.shrinkToFit_ = shrinkToFit;
677 this.resizeToClient_ = resizeToClient;
678 this.updateScrollbarVisibility();
680 if (this.hostId_ != '') {
682 options[remoting.ClientSession.KEY_SHRINK_TO_FIT] = this.shrinkToFit_;
683 options[remoting.ClientSession.KEY_RESIZE_TO_CLIENT] = this.resizeToClient_;
684 remoting.HostSettings.save(this.hostId_, options);
687 this.updateDimensions();
688 if (needsScrollReset) {
695 * Called when the client receives its first frame.
697 * @return {void} Nothing.
699 remoting.ClientSession.prototype.onFirstFrameReceived = function() {
700 this.hasReceivedFrame_ = true;
704 * @return {boolean} Whether the client has received a video buffer.
706 remoting.ClientSession.prototype.hasReceivedFrame = function() {
707 return this.hasReceivedFrame_;
711 * Sends an IQ stanza via the http xmpp proxy.
714 * @param {string} msg XML string of IQ stanza to send to server.
715 * @return {void} Nothing.
717 remoting.ClientSession.prototype.sendIq_ = function(msg) {
718 // Extract the session id, so we can close the session later.
719 var parser = new DOMParser();
720 var iqNode = parser.parseFromString(msg, 'text/xml').firstChild;
721 var jingleNode = iqNode.firstChild;
723 var action = jingleNode.getAttribute('action');
724 if (jingleNode.nodeName == 'jingle' && action == 'session-initiate') {
725 this.sessionId_ = jingleNode.getAttribute('sid');
729 // HACK: Add 'x' prefix to the IDs of the outgoing messages to make sure that
730 // stanza IDs used by host and client do not match. This is necessary to
731 // workaround bug in the signaling endpoint used by chromoting.
732 // TODO(sergeyu): Remove this hack once the server-side bug is fixed.
733 var type = iqNode.getAttribute('type');
735 var id = iqNode.getAttribute('id');
736 iqNode.setAttribute('id', 'x' + id);
737 msg = (new XMLSerializer()).serializeToString(iqNode);
740 console.log(remoting.timestamp(), remoting.formatIq.prettifySendIq(msg));
743 remoting.wcsSandbox.sendIq(msg);
746 remoting.ClientSession.prototype.initiateConnection_ = function() {
747 /** @type {remoting.ClientSession} */
750 remoting.wcsSandbox.connect(onWcsConnected, this.resetWithError_.bind(this));
752 /** @param {string} localJid Local JID. */
753 function onWcsConnected(localJid) {
754 that.connectPluginToWcs_(localJid);
755 that.getSharedSecret_(onSharedSecretReceived.bind(null, localJid));
758 /** @param {string} localJid Local JID.
759 * @param {string} sharedSecret Shared secret. */
760 function onSharedSecretReceived(localJid, sharedSecret) {
761 that.plugin_.connect(
762 that.hostJid_, that.hostPublicKey_, localJid, sharedSecret,
763 that.authenticationMethods_, that.hostId_, that.clientPairingId_,
764 that.clientPairedSecret_);
769 * Connects the plugin to WCS.
772 * @param {string} localJid Local JID.
773 * @return {void} Nothing.
775 remoting.ClientSession.prototype.connectPluginToWcs_ = function(localJid) {
776 remoting.formatIq.setJids(localJid, this.hostJid_);
777 var forwardIq = this.plugin_.onIncomingIq.bind(this.plugin_);
778 /** @param {string} stanza The IQ stanza received. */
779 var onIncomingIq = function(stanza) {
780 // HACK: Remove 'x' prefix added to the id in sendIq_().
782 var parser = new DOMParser();
783 var iqNode = parser.parseFromString(stanza, 'text/xml').firstChild;
784 var type = iqNode.getAttribute('type');
785 var id = iqNode.getAttribute('id');
786 if (type != 'set' && id.charAt(0) == 'x') {
787 iqNode.setAttribute('id', id.substr(1));
788 stanza = (new XMLSerializer()).serializeToString(iqNode);
791 // Pass message as is when it is malformed.
794 console.log(remoting.timestamp(),
795 remoting.formatIq.prettifyReceiveIq(stanza));
798 remoting.wcsSandbox.setOnIq(onIncomingIq);
802 * Gets shared secret to be used for connection.
804 * @param {function(string)} callback Callback called with the shared secret.
805 * @return {void} Nothing.
808 remoting.ClientSession.prototype.getSharedSecret_ = function(callback) {
809 /** @type remoting.ClientSession */
811 if (this.plugin_.hasFeature(remoting.ClientPlugin.Feature.THIRD_PARTY_AUTH)) {
812 /** @type{function(string, string, string): void} */
813 var fetchThirdPartyToken = function(tokenUrl, hostPublicKey, scope) {
814 that.fetchThirdPartyToken_(
815 tokenUrl, hostPublicKey, scope,
816 that.plugin_.onThirdPartyTokenFetched.bind(that.plugin_));
818 this.plugin_.fetchThirdPartyTokenHandler = fetchThirdPartyToken;
820 if (this.accessCode_) {
821 // Shared secret was already supplied before connecting (It2Me case).
822 callback(this.accessCode_);
823 } else if (this.plugin_.hasFeature(
824 remoting.ClientPlugin.Feature.ASYNC_PIN)) {
825 // Plugin supports asynchronously asking for the PIN.
826 this.plugin_.useAsyncPinDialog();
827 /** @param {boolean} pairingSupported */
828 var fetchPin = function(pairingSupported) {
829 that.fetchPin_(pairingSupported,
830 that.plugin_.onPinFetched.bind(that.plugin_));
832 this.plugin_.fetchPinHandler = fetchPin;
835 // Clients that don't support asking for a PIN asynchronously also don't
836 // support pairing, so request the PIN now without offering to remember it.
837 this.fetchPin_(false, callback);
842 * Callback that the plugin invokes to indicate that the connection
843 * status has changed.
846 * @param {number} status The plugin's status.
847 * @param {number} error The plugin's error state, if any.
849 remoting.ClientSession.prototype.onConnectionStatusUpdate_ =
850 function(status, error) {
851 if (status == remoting.ClientSession.State.CONNECTED) {
852 this.setFocusHandlers_();
853 this.onDesktopSizeChanged_();
854 if (this.resizeToClient_) {
855 this.plugin_.notifyClientResolution(window.innerWidth,
857 window.devicePixelRatio);
859 } else if (status == remoting.ClientSession.State.FAILED) {
861 case remoting.ClientSession.ConnectionError.HOST_IS_OFFLINE:
862 this.error_ = remoting.Error.HOST_IS_OFFLINE;
864 case remoting.ClientSession.ConnectionError.SESSION_REJECTED:
865 this.error_ = remoting.Error.INVALID_ACCESS_CODE;
867 case remoting.ClientSession.ConnectionError.INCOMPATIBLE_PROTOCOL:
868 this.error_ = remoting.Error.INCOMPATIBLE_PROTOCOL;
870 case remoting.ClientSession.ConnectionError.NETWORK_FAILURE:
871 this.error_ = remoting.Error.P2P_FAILURE;
873 case remoting.ClientSession.ConnectionError.HOST_OVERLOAD:
874 this.error_ = remoting.Error.HOST_OVERLOAD;
877 this.error_ = remoting.Error.UNEXPECTED;
880 this.setState_(/** @type {remoting.ClientSession.State} */ (status));
884 * Callback that the plugin invokes to indicate when the connection is
888 * @param {boolean} ready True if the connection is ready.
890 remoting.ClientSession.prototype.onConnectionReady_ = function(ready) {
892 this.plugin_.element().classList.add("session-client-inactive");
894 this.plugin_.element().classList.remove("session-client-inactive");
899 * Called when the client-host capabilities negotiation is complete.
901 * @param {!Array.<string>} capabilities The set of capabilities negotiated
902 * between the client and host.
903 * @return {void} Nothing.
906 remoting.ClientSession.prototype.onSetCapabilities_ = function(capabilities) {
907 if (this.capabilities_ != null) {
908 console.error('onSetCapabilities_() is called more than once');
912 this.capabilities_ = capabilities;
913 if (this.hasCapability_(
914 remoting.ClientSession.Capability.SEND_INITIAL_RESOLUTION)) {
915 this.plugin_.notifyClientResolution(window.innerWidth,
917 window.devicePixelRatio);
923 * @param {remoting.ClientSession.State} newState The new state for the session.
924 * @return {void} Nothing.
926 remoting.ClientSession.prototype.setState_ = function(newState) {
927 var oldState = this.state_;
928 this.state_ = newState;
929 var state = this.state_;
930 if (oldState == remoting.ClientSession.State.CONNECTING) {
931 if (this.state_ == remoting.ClientSession.State.CLOSED) {
932 state = remoting.ClientSession.State.CONNECTION_CANCELED;
933 } else if (this.state_ == remoting.ClientSession.State.FAILED &&
934 this.error_ == remoting.Error.HOST_IS_OFFLINE &&
935 !this.logHostOfflineErrors_) {
936 // The application requested host-offline errors to be suppressed, for
937 // example, because this connection attempt is using a cached host JID.
938 console.log('Suppressing host-offline error.');
939 state = remoting.ClientSession.State.CONNECTION_CANCELED;
941 } else if (oldState == remoting.ClientSession.State.CONNECTED &&
942 this.state_ == remoting.ClientSession.State.FAILED) {
943 state = remoting.ClientSession.State.CONNECTION_DROPPED;
945 this.logToServer.logClientSessionStateChange(state, this.error_, this.mode_);
946 if (this.onStateChange_) {
947 this.onStateChange_(oldState, newState);
952 * This is a callback that gets called when the window is resized.
954 * @return {void} Nothing.
956 remoting.ClientSession.prototype.onResize = function() {
957 this.updateDimensions();
959 if (this.notifyClientResolutionTimer_) {
960 window.clearTimeout(this.notifyClientResolutionTimer_);
961 this.notifyClientResolutionTimer_ = null;
964 // Defer notifying the host of the change until the window stops resizing, to
965 // avoid overloading the control channel with notifications.
966 if (this.resizeToClient_) {
967 var kResizeRateLimitMs = 1000;
968 if (this.hasCapability_(
969 remoting.ClientSession.Capability.RATE_LIMIT_RESIZE_REQUESTS)) {
970 kResizeRateLimitMs = 250;
972 this.notifyClientResolutionTimer_ = window.setTimeout(
973 this.plugin_.notifyClientResolution.bind(this.plugin_,
976 window.devicePixelRatio),
980 // If bump-scrolling is enabled, adjust the plugin margins to fully utilize
981 // the new window area.
984 this.updateScrollbarVisibility();
988 * Requests that the host pause or resume video updates.
990 * @param {boolean} pause True to pause video, false to resume.
991 * @return {void} Nothing.
993 remoting.ClientSession.prototype.pauseVideo = function(pause) {
995 this.plugin_.pauseVideo(pause)
1000 * Requests that the host pause or resume audio.
1002 * @param {boolean} pause True to pause audio, false to resume.
1003 * @return {void} Nothing.
1005 remoting.ClientSession.prototype.pauseAudio = function(pause) {
1007 this.plugin_.pauseAudio(pause)
1012 * This is a callback that gets called when the plugin notifies us of a change
1013 * in the size of the remote desktop.
1016 * @return {void} Nothing.
1018 remoting.ClientSession.prototype.onDesktopSizeChanged_ = function() {
1019 console.log('desktop size changed: ' +
1020 this.plugin_.desktopWidth + 'x' +
1021 this.plugin_.desktopHeight +' @ ' +
1022 this.plugin_.desktopXDpi + 'x' +
1023 this.plugin_.desktopYDpi + ' DPI');
1024 this.updateDimensions();
1025 this.updateScrollbarVisibility();
1029 * Refreshes the plugin's dimensions, taking into account the sizes of the
1030 * remote desktop and client window, and the current scale-to-fit setting.
1032 * @return {void} Nothing.
1034 remoting.ClientSession.prototype.updateDimensions = function() {
1035 if (this.plugin_.desktopWidth == 0 ||
1036 this.plugin_.desktopHeight == 0) {
1040 var windowWidth = window.innerWidth;
1041 var windowHeight = window.innerHeight;
1042 var desktopWidth = this.plugin_.desktopWidth;
1043 var desktopHeight = this.plugin_.desktopHeight;
1045 // When configured to display a host at its original size, we aim to display
1046 // it as close to its physical size as possible, without losing data:
1047 // - If client and host have matching DPI, render the host pixel-for-pixel.
1048 // - If the host has higher DPI then still render pixel-for-pixel.
1049 // - If the host has lower DPI then let Chrome up-scale it to natural size.
1051 // We specify the plugin dimensions in Density-Independent Pixels, so to
1052 // render pixel-for-pixel we need to down-scale the host dimensions by the
1053 // devicePixelRatio of the client. To match the host pixel density, we choose
1054 // an initial scale factor based on the client devicePixelRatio and host DPI.
1056 // Determine the effective device pixel ratio of the host, based on DPI.
1057 var hostPixelRatioX = Math.ceil(this.plugin_.desktopXDpi / 96);
1058 var hostPixelRatioY = Math.ceil(this.plugin_.desktopYDpi / 96);
1059 var hostPixelRatio = Math.min(hostPixelRatioX, hostPixelRatioY);
1061 // Down-scale by the smaller of the client and host ratios.
1062 var scale = 1.0 / Math.min(window.devicePixelRatio, hostPixelRatio);
1064 if (this.shrinkToFit_) {
1065 // Reduce the scale, if necessary, to fit the whole desktop in the window.
1066 var scaleFitWidth = Math.min(scale, 1.0 * windowWidth / desktopWidth);
1067 var scaleFitHeight = Math.min(scale, 1.0 * windowHeight / desktopHeight);
1068 scale = Math.min(scaleFitHeight, scaleFitWidth);
1070 // If we're running full-screen then try to handle common side-by-side
1071 // multi-monitor combinations more intelligently.
1072 if (document.webkitIsFullScreen) {
1073 // If the host has two monitors each the same size as the client then
1074 // scale-to-fit will have the desktop occupy only 50% of the client area,
1075 // in which case it would be preferable to down-scale less and let the
1076 // user bump-scroll around ("scale-and-pan").
1077 // Triggering scale-and-pan if less than 65% of the client area would be
1078 // used adds enough fuzz to cope with e.g. 1280x800 client connecting to
1079 // a (2x1280)x1024 host nicely.
1080 // Note that we don't need to account for scrollbars while fullscreen.
1081 if (scale <= scaleFitHeight * 0.65) {
1082 scale = scaleFitHeight;
1084 if (scale <= scaleFitWidth * 0.65) {
1085 scale = scaleFitWidth;
1090 var pluginWidth = desktopWidth * scale;
1091 var pluginHeight = desktopHeight * scale;
1093 // Resize the plugin if necessary.
1094 // TODO(wez): Handle high-DPI to high-DPI properly (crbug.com/135089).
1095 this.plugin_.element().width = pluginWidth;
1096 this.plugin_.element().height = pluginHeight;
1098 // Position the container.
1099 // Note that clientWidth/Height take into account scrollbars.
1100 var clientWidth = document.documentElement.clientWidth;
1101 var clientHeight = document.documentElement.clientHeight;
1102 var parentNode = this.plugin_.element().parentNode;
1104 console.log('plugin dimensions: ' +
1105 parentNode.style.left + ',' +
1106 parentNode.style.top + '-' +
1107 pluginWidth + 'x' + pluginHeight + '.');
1111 * Returns an associative array with a set of stats for this connection.
1113 * @return {remoting.ClientSession.PerfStats} The connection statistics.
1115 remoting.ClientSession.prototype.getPerfStats = function() {
1116 return this.plugin_.getPerfStats();
1122 * @param {remoting.ClientSession.PerfStats} stats
1124 remoting.ClientSession.prototype.logStatistics = function(stats) {
1125 this.logToServer.logStatistics(stats, this.mode_);
1129 * Enable or disable logging of connection errors due to a host being offline.
1130 * For example, if attempting a connection using a cached JID, host-offline
1131 * errors should not be logged because the JID will be refreshed and the
1132 * connection retried.
1134 * @param {boolean} enable True to log host-offline errors; false to suppress.
1136 remoting.ClientSession.prototype.logHostOfflineErrors = function(enable) {
1137 this.logHostOfflineErrors_ = enable;
1141 * Request pairing with the host for PIN-less authentication.
1143 * @param {string} clientName The human-readable name of the client.
1144 * @param {function(string, string):void} onDone Callback to receive the
1145 * client id and shared secret when they are available.
1147 remoting.ClientSession.prototype.requestPairing = function(clientName, onDone) {
1149 this.plugin_.requestPairing(clientName, onDone);
1154 * Toggles between full-screen and windowed mode.
1155 * @return {void} Nothing.
1158 remoting.ClientSession.prototype.toggleFullScreen_ = function() {
1159 var htmlNode = /** @type {HTMLElement} */ (document.body.parentNode);
1160 if (document.webkitIsFullScreen) {
1161 document.webkitCancelFullScreen();
1162 this.enableBumpScroll_(false);
1163 htmlNode.classList.remove('full-screen');
1165 document.body.webkitRequestFullScreen(Element.ALLOW_KEYBOARD_INPUT);
1166 // Don't enable bump scrolling immediately because it can result in
1167 // onMouseMove firing before the webkitIsFullScreen property can be
1168 // read safely (crbug.com/132180).
1169 window.setTimeout(this.enableBumpScroll_.bind(this, true), 0);
1170 htmlNode.classList.add('full-screen');
1175 * Updates the options menu to reflect the current scale-to-fit and full-screen
1177 * @return {void} Nothing.
1180 remoting.ClientSession.prototype.onShowOptionsMenu_ = function() {
1181 remoting.MenuButton.select(this.resizeToClientButton_, this.resizeToClient_);
1182 remoting.MenuButton.select(this.shrinkToFitButton_, this.shrinkToFit_);
1183 remoting.MenuButton.select(this.fullScreenButton_,
1184 document.webkitIsFullScreen);
1188 * Scroll the client plugin by the specified amount, keeping it visible.
1189 * Note that this is only used in content full-screen mode (not windowed or
1190 * browser full-screen modes), where window.scrollBy and the scrollTop and
1191 * scrollLeft properties don't work.
1192 * @param {number} dx The amount by which to scroll horizontally. Positive to
1193 * scroll right; negative to scroll left.
1194 * @param {number} dy The amount by which to scroll vertically. Positive to
1195 * scroll down; negative to scroll up.
1196 * @return {boolean} True if the requested scroll had no effect because both
1197 * vertical and horizontal edges of the screen have been reached.
1200 remoting.ClientSession.prototype.scroll_ = function(dx, dy) {
1201 var plugin = this.plugin_.element();
1202 var style = plugin.style;
1205 * Helper function for x- and y-scrolling
1206 * @param {number|string} curr The current margin, eg. "10px".
1207 * @param {number} delta The requested scroll amount.
1208 * @param {number} windowBound The size of the window, in pixels.
1209 * @param {number} pluginBound The size of the plugin, in pixels.
1210 * @param {{stop: boolean}} stop Reference parameter used to indicate when
1211 * the scroll has reached one of the edges and can be stopped in that
1213 * @return {string} The new margin value.
1215 var adjustMargin = function(curr, delta, windowBound, pluginBound, stop) {
1216 var minMargin = Math.min(0, windowBound - pluginBound);
1217 var result = (curr ? parseFloat(curr) : 0) - delta;
1218 result = Math.min(0, Math.max(minMargin, result));
1219 stop.stop = (result == 0 || result == minMargin);
1220 return result + "px";
1223 var stopX = { stop: false };
1224 style.marginLeft = adjustMargin(style.marginLeft, dx,
1225 window.innerWidth, plugin.width, stopX);
1226 var stopY = { stop: false };
1227 style.marginTop = adjustMargin(style.marginTop, dy,
1228 window.innerHeight, plugin.height, stopY);
1229 return stopX.stop && stopY.stop;
1233 * Enable or disable bump-scrolling. When disabling bump scrolling, also reset
1234 * the scroll offsets to (0, 0).
1236 * @param {boolean} enable True to enable bump-scrolling, false to disable it.
1238 remoting.ClientSession.prototype.enableBumpScroll_ = function(enable) {
1240 /** @type {null|function(Event):void} */
1241 this.onMouseMoveRef_ = this.onMouseMove_.bind(this);
1242 this.plugin_.element().addEventListener(
1243 'mousemove', this.onMouseMoveRef_, false);
1245 this.plugin_.element().removeEventListener(
1246 'mousemove', this.onMouseMoveRef_, false);
1247 this.onMouseMoveRef_ = null;
1248 this.plugin_.element().style.marginLeft = 0;
1249 this.plugin_.element().style.marginTop = 0;
1254 * @param {Event} event The mouse event.
1257 remoting.ClientSession.prototype.onMouseMove_ = function(event) {
1258 if (this.bumpScrollTimer_) {
1259 window.clearTimeout(this.bumpScrollTimer_);
1260 this.bumpScrollTimer_ = null;
1262 // It's possible to leave content full-screen mode without using the Screen
1263 // Options menu, so we disable bump scrolling as soon as we detect this.
1264 if (!document.webkitIsFullScreen) {
1265 this.enableBumpScroll_(false);
1269 * Compute the scroll speed based on how close the mouse is to the edge.
1270 * @param {number} mousePos The mouse x- or y-coordinate
1271 * @param {number} size The width or height of the content area.
1272 * @return {number} The scroll delta, in pixels.
1274 var computeDelta = function(mousePos, size) {
1276 if (mousePos >= size - threshold) {
1277 return 1 + 5 * (mousePos - (size - threshold)) / threshold;
1278 } else if (mousePos <= threshold) {
1279 return -1 - 5 * (threshold - mousePos) / threshold;
1284 var dx = computeDelta(event.x, window.innerWidth);
1285 var dy = computeDelta(event.y, window.innerHeight);
1287 if (dx != 0 || dy != 0) {
1288 /** @type {remoting.ClientSession} */
1291 * Scroll the view, and schedule a timer to do so again unless we've hit
1292 * the edges of the screen. This timer is cancelled when the mouse moves.
1293 * @param {number} expected The time at which we expect to be called.
1295 var repeatScroll = function(expected) {
1296 /** @type {number} */
1297 var now = new Date().getTime();
1298 /** @type {number} */
1300 var lateAdjustment = 1 + (now - expected) / timeout;
1301 if (!that.scroll_(lateAdjustment * dx, lateAdjustment * dy)) {
1302 that.bumpScrollTimer_ = window.setTimeout(
1303 function() { repeatScroll(now + timeout); },
1307 repeatScroll(new Date().getTime());
1312 * Sends a clipboard item to the host.
1314 * @param {string} mimeType The MIME type of the clipboard item.
1315 * @param {string} item The clipboard item.
1317 remoting.ClientSession.prototype.sendClipboardItem = function(mimeType, item) {
1320 this.plugin_.sendClipboardItem(mimeType, item)