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} hostJid The jid of the host to connect to.
27 * @param {string} clientJid The jid of the WCS client.
28 * @param {string} hostPublicKey The base64 encoded version of the host's
30 * @param {string} accessCode The IT2Me access code. Blank for Me2Me.
31 * @param {function(function(string): void): void} fetchPin Called by Me2Me
32 * connections when a PIN needs to be obtained interactively.
33 * @param {string} authenticationMethods Comma-separated list of
34 * authentication methods the client should attempt to use.
35 * @param {string} hostId The host identifier for Me2Me, or empty for IT2Me.
36 * Mixed into authentication hashes for some authentication methods.
37 * @param {remoting.ClientSession.Mode} mode The mode of this connection.
38 * @param {string} hostDisplayName The name of the host for display purposes.
41 remoting
.ClientSession = function(hostJid
, clientJid
, hostPublicKey
, accessCode
,
42 fetchPin
, authenticationMethods
, hostId
,
43 mode
, hostDisplayName
) {
44 this.state
= remoting
.ClientSession
.State
.CREATED
;
46 this.hostJid
= hostJid
;
47 this.clientJid
= clientJid
;
48 this.hostPublicKey
= hostPublicKey
;
50 this.accessCode_
= accessCode
;
52 this.fetchPin_
= fetchPin
;
53 this.authenticationMethods
= authenticationMethods
;
56 this.hostDisplayName
= hostDisplayName
;
57 /** @type {remoting.ClientSession.Mode} */
60 /** @type {remoting.ClientPlugin} */
63 this.shrinkToFit_
= true;
65 this.resizeToClient_
= false;
69 this.hasReceivedFrame_
= false;
70 this.logToServer
= new remoting
.LogToServer();
71 /** @type {?function(remoting.ClientSession.State,
72 remoting.ClientSession.State):void} */
73 this.onStateChange_
= null;
75 /** @type {number?} @private */
76 this.notifyClientResolutionTimer_
= null;
77 /** @type {number?} @private */
78 this.bumpScrollTimer_
= null;
81 * Allow host-offline error reporting to be suppressed in situations where it
82 * would not be useful, for example, when using a cached host JID.
84 * @type {boolean} @private
86 this.logHostOfflineErrors_
= true;
89 this.callPluginLostFocus_
= this.pluginLostFocus_
.bind(this);
91 this.callPluginGotFocus_
= this.pluginGotFocus_
.bind(this);
93 this.callSetScreenMode_
= this.onSetScreenMode_
.bind(this);
95 this.callToggleFullScreen_
= this.toggleFullScreen_
.bind(this);
98 this.screenOptionsMenu_
= new remoting
.MenuButton(
99 document
.getElementById('screen-options-menu'),
100 this.onShowOptionsMenu_
.bind(this));
102 this.sendKeysMenu_
= new remoting
.MenuButton(
103 document
.getElementById('send-keys-menu')
106 /** @type {HTMLElement} @private */
107 this.resizeToClientButton_
=
108 document
.getElementById('screen-resize-to-client');
109 /** @type {HTMLElement} @private */
110 this.shrinkToFitButton_
= document
.getElementById('screen-shrink-to-fit');
111 /** @type {HTMLElement} @private */
112 this.fullScreenButton_
= document
.getElementById('toggle-full-screen');
114 if (this.mode
== remoting
.ClientSession
.Mode
.IT2ME
) {
115 // Resize-to-client is not supported for IT2Me hosts.
116 this.resizeToClientButton_
.hidden
= true;
118 this.resizeToClientButton_
.hidden
= false;
119 this.resizeToClientButton_
.addEventListener(
120 'click', this.callSetScreenMode_
, false);
123 this.shrinkToFitButton_
.addEventListener(
124 'click', this.callSetScreenMode_
, false);
125 this.fullScreenButton_
.addEventListener(
126 'click', this.callToggleFullScreen_
, false);
130 * @param {?function(remoting.ClientSession.State,
131 remoting.ClientSession.State):void} onStateChange
132 * The callback to invoke when the session changes state.
134 remoting
.ClientSession
.prototype.setOnStateChange = function(onStateChange
) {
135 this.onStateChange_
= onStateChange
;
138 // Note that the positive values in both of these enums are copied directly
139 // from chromoting_scriptable_object.h and must be kept in sync. The negative
140 // values represent state transitions that occur within the web-app that have
141 // no corresponding plugin state transition.
142 /** @enum {number} */
143 remoting
.ClientSession
.State
= {
144 CONNECTION_CANCELED
: -5, // Connection closed (gracefully) before connecting.
145 CONNECTION_DROPPED
: -4, // Succeeded, but subsequently closed with an error.
147 BAD_PLUGIN_VERSION
: -2,
148 UNKNOWN_PLUGIN_ERROR
: -1,
157 /** @enum {number} */
158 remoting
.ClientSession
.ConnectionError
= {
163 INCOMPATIBLE_PROTOCOL
: 3,
168 // The mode of this session.
169 /** @enum {number} */
170 remoting
.ClientSession
.Mode
= {
176 * Type used for performance statistics collected by the plugin.
179 remoting
.ClientSession
.PerfStats = function() {};
180 /** @type {number} */
181 remoting
.ClientSession
.PerfStats
.prototype.videoBandwidth
;
182 /** @type {number} */
183 remoting
.ClientSession
.PerfStats
.prototype.videoFrameRate
;
184 /** @type {number} */
185 remoting
.ClientSession
.PerfStats
.prototype.captureLatency
;
186 /** @type {number} */
187 remoting
.ClientSession
.PerfStats
.prototype.encodeLatency
;
188 /** @type {number} */
189 remoting
.ClientSession
.PerfStats
.prototype.decodeLatency
;
190 /** @type {number} */
191 remoting
.ClientSession
.PerfStats
.prototype.renderLatency
;
192 /** @type {number} */
193 remoting
.ClientSession
.PerfStats
.prototype.roundtripLatency
;
195 // Keys for connection statistics.
196 remoting
.ClientSession
.STATS_KEY_VIDEO_BANDWIDTH
= 'videoBandwidth';
197 remoting
.ClientSession
.STATS_KEY_VIDEO_FRAME_RATE
= 'videoFrameRate';
198 remoting
.ClientSession
.STATS_KEY_CAPTURE_LATENCY
= 'captureLatency';
199 remoting
.ClientSession
.STATS_KEY_ENCODE_LATENCY
= 'encodeLatency';
200 remoting
.ClientSession
.STATS_KEY_DECODE_LATENCY
= 'decodeLatency';
201 remoting
.ClientSession
.STATS_KEY_RENDER_LATENCY
= 'renderLatency';
202 remoting
.ClientSession
.STATS_KEY_ROUNDTRIP_LATENCY
= 'roundtripLatency';
204 // Keys for per-host settings.
205 remoting
.ClientSession
.KEY_REMAP_KEYS
= 'remapKeys';
206 remoting
.ClientSession
.KEY_RESIZE_TO_CLIENT
= 'resizeToClient';
207 remoting
.ClientSession
.KEY_SHRINK_TO_FIT
= 'shrinkToFit';
210 * The current state of the session.
211 * @type {remoting.ClientSession.State}
213 remoting
.ClientSession
.prototype.state
= remoting
.ClientSession
.State
.UNKNOWN
;
216 * The last connection error. Set when state is set to FAILED.
217 * @type {remoting.ClientSession.ConnectionError}
220 remoting
.ClientSession
.prototype.error_
=
221 remoting
.ClientSession
.ConnectionError
.NONE
;
224 * The id of the client plugin
228 remoting
.ClientSession
.prototype.PLUGIN_ID
= 'session-client-plugin';
231 * @param {Element} container The element to add the plugin to.
232 * @param {string} id Id to use for the plugin element .
233 * @return {remoting.ClientPlugin} Create plugin object for the locally
236 remoting
.ClientSession
.prototype.createClientPlugin_ = function(container
, id
) {
237 var plugin
= /** @type {remoting.ViewerPlugin} */
238 document
.createElement('embed');
241 plugin
.src
= 'about://none';
242 plugin
.type
= 'application/vnd.chromium.remoting-viewer';
245 plugin
.tabIndex
= 0; // Required, otherwise focus() doesn't work.
246 container
.appendChild(plugin
);
248 return new remoting
.ClientPluginAsync(plugin
);
252 * Callback function called when the plugin element gets focus.
254 remoting
.ClientSession
.prototype.pluginGotFocus_ = function() {
255 remoting
.clipboard
.initiateToHost();
259 * Callback function called when the plugin element loses focus.
261 remoting
.ClientSession
.prototype.pluginLostFocus_ = function() {
263 // Release all keys to prevent them becoming 'stuck down' on the host.
264 this.plugin
.releaseAllKeys();
265 if (this.plugin
.element()) {
266 // Focus should stay on the element, not (for example) the toolbar.
267 this.plugin
.element().focus();
273 * Adds <embed> element to |container| and readies the sesion object.
275 * @param {Element} container The element to add the plugin to.
277 remoting
.ClientSession
.prototype.createPluginAndConnect
=
278 function(container
) {
279 this.plugin
= this.createClientPlugin_(container
, this.PLUGIN_ID
);
280 remoting
.HostSettings
.load(this.hostId
,
281 this.onHostSettingsLoaded_
.bind(this));
285 * @param {Object.<string>} options The current options for the host, or {}
286 * if this client has no saved settings for the host.
289 remoting
.ClientSession
.prototype.onHostSettingsLoaded_ = function(options
) {
290 if (remoting
.ClientSession
.KEY_REMAP_KEYS
in options
&&
291 typeof(options
[remoting
.ClientSession
.KEY_REMAP_KEYS
]) ==
293 this.remapKeys_
= /** @type {string} */
294 options
[remoting
.ClientSession
.KEY_REMAP_KEYS
];
296 if (remoting
.ClientSession
.KEY_RESIZE_TO_CLIENT
in options
&&
297 typeof(options
[remoting
.ClientSession
.KEY_RESIZE_TO_CLIENT
]) ==
299 this.resizeToClient_
= /** @type {boolean} */
300 options
[remoting
.ClientSession
.KEY_RESIZE_TO_CLIENT
];
302 if (remoting
.ClientSession
.KEY_SHRINK_TO_FIT
in options
&&
303 typeof(options
[remoting
.ClientSession
.KEY_SHRINK_TO_FIT
]) ==
305 this.shrinkToFit_
= /** @type {boolean} */
306 options
[remoting
.ClientSession
.KEY_SHRINK_TO_FIT
];
309 /** @param {boolean} result */
310 this.plugin
.initialize(this.onPluginInitialized_
.bind(this));
314 * Constrains the focus to the plugin element.
317 remoting
.ClientSession
.prototype.setFocusHandlers_ = function() {
318 this.plugin
.element().addEventListener(
319 'focus', this.callPluginGotFocus_
, false);
320 this.plugin
.element().addEventListener(
321 'blur', this.callPluginLostFocus_
, false);
322 this.plugin
.element().focus();
326 * @param {boolean} initialized
328 remoting
.ClientSession
.prototype.onPluginInitialized_ = function(initialized
) {
330 console
.error('ERROR: remoting plugin not loaded');
331 this.plugin
.cleanup();
333 this.setState_(remoting
.ClientSession
.State
.UNKNOWN_PLUGIN_ERROR
);
337 if (!this.plugin
.isSupportedVersion()) {
338 this.plugin
.cleanup();
340 this.setState_(remoting
.ClientSession
.State
.BAD_PLUGIN_VERSION
);
344 // Show the Send Keys menu only if the plugin has the injectKeyEvent feature,
345 // and the Ctrl-Alt-Del button only in Me2Me mode.
346 if (!this.plugin
.hasFeature(remoting
.ClientPlugin
.Feature
.INJECT_KEY_EVENT
)) {
347 var sendKeysElement
= document
.getElementById('send-keys-menu');
348 sendKeysElement
.hidden
= true;
349 } else if (this.mode
!= remoting
.ClientSession
.Mode
.ME2ME
) {
350 var sendCadElement
= document
.getElementById('send-ctrl-alt-del');
351 sendCadElement
.hidden
= true;
354 // Apply customized key remappings if the plugin supports remapKeys.
355 if (this.plugin
.hasFeature(remoting
.ClientPlugin
.Feature
.REMAP_KEY
)) {
356 this.applyRemapKeys_(true);
359 /** @param {string} msg The IQ stanza to send. */
360 this.plugin
.onOutgoingIqHandler
= this.sendIq_
.bind(this);
361 /** @param {string} msg The message to log. */
362 this.plugin
.onDebugMessageHandler = function(msg
) {
363 console
.log('plugin: ' + msg
);
366 this.plugin
.onConnectionStatusUpdateHandler
=
367 this.onConnectionStatusUpdate_
.bind(this);
368 this.plugin
.onConnectionReadyHandler
=
369 this.onConnectionReady_
.bind(this);
370 this.plugin
.onDesktopSizeUpdateHandler
=
371 this.onDesktopSizeChanged_
.bind(this);
373 this.connectPluginToWcs_();
377 * Deletes the <embed> element from the container, without sending a
378 * session_terminate request. This is to be called when the session was
379 * disconnected by the Host.
381 * @return {void} Nothing.
383 remoting
.ClientSession
.prototype.removePlugin = function() {
385 this.plugin
.element().removeEventListener(
386 'focus', this.callPluginGotFocus_
, false);
387 this.plugin
.element().removeEventListener(
388 'blur', this.callPluginLostFocus_
, false);
389 this.plugin
.cleanup();
392 this.resizeToClientButton_
.removeEventListener(
393 'click', this.callSetScreenMode_
, false);
394 this.shrinkToFitButton_
.removeEventListener(
395 'click', this.callSetScreenMode_
, false);
396 this.fullScreenButton_
.removeEventListener(
397 'click', this.callToggleFullScreen_
, false);
401 * Deletes the <embed> element from the container and disconnects.
403 * @param {boolean} isUserInitiated True for user-initiated disconnects, False
404 * for disconnects due to connection failures.
405 * @return {void} Nothing.
407 remoting
.ClientSession
.prototype.disconnect = function(isUserInitiated
) {
408 if (isUserInitiated
) {
409 // The plugin won't send a state change notification, so we explicitly log
410 // the fact that the connection has closed.
411 this.logToServer
.logClientSessionStateChange(
412 remoting
.ClientSession
.State
.CLOSED
,
413 remoting
.ClientSession
.ConnectionError
.NONE
, this.mode
);
415 remoting
.wcsSandbox
.setOnIq(null);
418 'to="' + this.hostJid
+ '" ' +
420 'id="session-terminate" ' +
421 'xmlns:cli="jabber:client">' +
423 'xmlns="urn:xmpp:jingle:1" ' +
424 'action="session-terminate" ' +
425 'initiator="' + this.clientJid
+ '" ' +
426 'sid="' + this.sessionId
+ '">' +
427 '<reason><success/></reason>' +
434 * @return {?remoting.Error} The current error code, or null if the connection
435 * is not in an error state.
437 remoting
.ClientSession
.prototype.getError = function() {
438 switch (this.error_
) {
439 case remoting
.ClientSession
.ConnectionError
.HOST_IS_OFFLINE
:
440 return remoting
.Error
.HOST_IS_OFFLINE
;
441 case remoting
.ClientSession
.ConnectionError
.SESSION_REJECTED
:
442 return remoting
.Error
.INVALID_ACCESS_CODE
;
443 case remoting
.ClientSession
.ConnectionError
.INCOMPATIBLE_PROTOCOL
:
444 return remoting
.Error
.INCOMPATIBLE_PROTOCOL
;
445 case remoting
.ClientSession
.ConnectionError
.NETWORK_FAILURE
:
446 return remoting
.Error
.P2P_FAILURE
;
447 case remoting
.ClientSession
.ConnectionError
.HOST_OVERLOAD
:
448 return remoting
.Error
.HOST_OVERLOAD
;
455 * Sends a key combination to the remoting client, by sending down events for
456 * the given keys, followed by up events in reverse order.
459 * @param {[number]} keys Key codes to be sent.
460 * @return {void} Nothing.
462 remoting
.ClientSession
.prototype.sendKeyCombination_ = function(keys
) {
463 for (var i
= 0; i
< keys
.length
; i
++) {
464 this.plugin
.injectKeyEvent(keys
[i
], true);
466 for (var i
= 0; i
< keys
.length
; i
++) {
467 this.plugin
.injectKeyEvent(keys
[i
], false);
472 * Sends a Ctrl-Alt-Del sequence to the remoting client.
474 * @return {void} Nothing.
476 remoting
.ClientSession
.prototype.sendCtrlAltDel = function() {
477 this.sendKeyCombination_([0x0700e0, 0x0700e2, 0x07004c]);
481 * Sends a Print Screen keypress to the remoting client.
483 * @return {void} Nothing.
485 remoting
.ClientSession
.prototype.sendPrintScreen = function() {
486 this.sendKeyCombination_([0x070046]);
490 * Sets and stores the key remapping setting for the current host.
492 * @param {string} remappings Comma separated list of key remappings.
494 remoting
.ClientSession
.prototype.setRemapKeys = function(remappings
) {
495 // Cancel any existing remappings and apply the new ones.
496 this.applyRemapKeys_(false);
497 this.remapKeys_
= remappings
;
498 this.applyRemapKeys_(true);
500 // Save the new remapping setting.
502 options
[remoting
.ClientSession
.KEY_REMAP_KEYS
] = this.remapKeys_
;
503 remoting
.HostSettings
.save(this.hostId
, options
);
507 * Applies the configured key remappings to the session, or resets them.
509 * @param {boolean} apply True to apply remappings, false to cancel them.
511 remoting
.ClientSession
.prototype.applyRemapKeys_ = function(apply
) {
512 // By default, under ChromeOS, remap the right Control key to the right
514 var remapKeys
= this.remapKeys_
;
515 if (remapKeys
== '' && remoting
.runningOnChromeOS()) {
516 remapKeys
= '0x0700e4>0x0700e7';
519 var remappings
= remapKeys
.split(',');
520 for (var i
= 0; i
< remappings
.length
; ++i
) {
521 var keyCodes
= remappings
[i
].split('>');
522 if (keyCodes
.length
!= 2) {
523 console
.log('bad remapKey: ' + remappings
[i
]);
526 var fromKey
= parseInt(keyCodes
[0], 0);
527 var toKey
= parseInt(keyCodes
[1], 0);
528 if (!fromKey
|| !toKey
) {
529 console
.log('bad remapKey code: ' + remappings
[i
]);
533 console
.log('remapKey 0x' + fromKey
.toString(16) +
534 '>0x' + toKey
.toString(16));
535 this.plugin
.remapKey(fromKey
, toKey
);
537 console
.log('cancel remapKey 0x' + fromKey
.toString(16));
538 this.plugin
.remapKey(fromKey
, fromKey
);
544 * Callback for the two "screen mode" related menu items: Resize desktop to
545 * fit and Shrink to fit.
547 * @param {Event} event The click event indicating which mode was selected.
548 * @return {void} Nothing.
551 remoting
.ClientSession
.prototype.onSetScreenMode_ = function(event
) {
552 var shrinkToFit
= this.shrinkToFit_
;
553 var resizeToClient
= this.resizeToClient_
;
554 if (event
.target
== this.shrinkToFitButton_
) {
555 shrinkToFit
= !shrinkToFit
;
557 if (event
.target
== this.resizeToClientButton_
) {
558 resizeToClient
= !resizeToClient
;
560 this.setScreenMode_(shrinkToFit
, resizeToClient
);
564 * Set the shrink-to-fit and resize-to-client flags and save them if this is
565 * a Me2Me connection.
567 * @param {boolean} shrinkToFit True if the remote desktop should be scaled
568 * down if it is larger than the client window; false if scroll-bars
569 * should be added in this case.
570 * @param {boolean} resizeToClient True if window resizes should cause the
571 * host to attempt to resize its desktop to match the client window size;
572 * false to disable this behaviour for subsequent window resizes--the
573 * current host desktop size is not restored in this case.
574 * @return {void} Nothing.
577 remoting
.ClientSession
.prototype.setScreenMode_
=
578 function(shrinkToFit
, resizeToClient
) {
579 if (resizeToClient
&& !this.resizeToClient_
) {
580 this.plugin
.notifyClientResolution(window
.innerWidth
,
582 window
.devicePixelRatio
);
585 // If enabling shrink, reset bump-scroll offsets.
586 var needsScrollReset
= shrinkToFit
&& !this.shrinkToFit_
;
588 this.shrinkToFit_
= shrinkToFit
;
589 this.resizeToClient_
= resizeToClient
;
591 if (this.hostId
!= '') {
593 options
[remoting
.ClientSession
.KEY_SHRINK_TO_FIT
] = this.shrinkToFit_
;
594 options
[remoting
.ClientSession
.KEY_RESIZE_TO_CLIENT
] = this.resizeToClient_
;
595 remoting
.HostSettings
.save(this.hostId
, options
);
598 this.updateDimensions();
599 if (needsScrollReset
) {
605 * Called when the client receives its first frame.
607 * @return {void} Nothing.
609 remoting
.ClientSession
.prototype.onFirstFrameReceived = function() {
610 this.hasReceivedFrame_
= true;
614 * @return {boolean} Whether the client has received a video buffer.
616 remoting
.ClientSession
.prototype.hasReceivedFrame = function() {
617 return this.hasReceivedFrame_
;
621 * Sends an IQ stanza via the http xmpp proxy.
624 * @param {string} msg XML string of IQ stanza to send to server.
625 * @return {void} Nothing.
627 remoting
.ClientSession
.prototype.sendIq_ = function(msg
) {
628 // Extract the session id, so we can close the session later.
629 var parser
= new DOMParser();
630 var iqNode
= parser
.parseFromString(msg
, 'text/xml').firstChild
;
631 var jingleNode
= iqNode
.firstChild
;
633 var action
= jingleNode
.getAttribute('action');
634 if (jingleNode
.nodeName
== 'jingle' && action
== 'session-initiate') {
635 this.sessionId
= jingleNode
.getAttribute('sid');
639 // HACK: Add 'x' prefix to the IDs of the outgoing messages to make sure that
640 // stanza IDs used by host and client do not match. This is necessary to
641 // workaround bug in the signaling endpoint used by chromoting.
642 // TODO(sergeyu): Remove this hack once the server-side bug is fixed.
643 var type
= iqNode
.getAttribute('type');
645 var id
= iqNode
.getAttribute('id');
646 iqNode
.setAttribute('id', 'x' + id
);
647 msg
= (new XMLSerializer()).serializeToString(iqNode
);
650 console
.log(remoting
.timestamp(), remoting
.formatIq
.prettifySendIq(msg
));
653 remoting
.wcsSandbox
.sendIq(msg
);
657 * Connects the plugin to WCS.
660 * @return {void} Nothing.
662 remoting
.ClientSession
.prototype.connectPluginToWcs_ = function() {
663 remoting
.formatIq
.setJids(this.clientJid
, this.hostJid
);
664 /** @type {remoting.ClientPlugin} */
665 var plugin
= this.plugin
;
666 var forwardIq
= plugin
.onIncomingIq
.bind(plugin
);
667 /** @param {string} stanza The IQ stanza received. */
668 var onIncomingIq = function(stanza
) {
669 // HACK: Remove 'x' prefix added to the id in sendIq_().
671 var parser
= new DOMParser();
672 var iqNode
= parser
.parseFromString(stanza
, 'text/xml').firstChild
;
673 var type
= iqNode
.getAttribute('type');
674 var id
= iqNode
.getAttribute('id');
675 if (type
!= 'set' && id
.charAt(0) == 'x') {
676 iqNode
.setAttribute('id', id
.substr(1));
677 stanza
= (new XMLSerializer()).serializeToString(iqNode
);
680 // Pass message as is when it is malformed.
683 console
.log(remoting
.timestamp(),
684 remoting
.formatIq
.prettifyReceiveIq(stanza
));
687 remoting
.wcsSandbox
.setOnIq(onIncomingIq
);
689 if (this.accessCode_
) {
690 // Shared secret was already supplied before connecting (It2Me case).
691 this.connectToHost_(this.accessCode_
);
693 } else if (plugin
.hasFeature(
694 remoting
.ClientPlugin
.Feature
.ASYNC_PIN
)) {
695 // Plugin supports asynchronously asking for the PIN.
696 plugin
.useAsyncPinDialog();
697 /** @type remoting.ClientSession */
699 var fetchPin = function() {
700 that
.fetchPin_(plugin
.onPinFetched
.bind(plugin
));
702 plugin
.fetchPinHandler
= fetchPin
;
703 this.connectToHost_('');
706 // Plugin doesn't support asynchronously asking for the PIN, ask now.
707 this.fetchPin_(this.connectToHost_
.bind(this));
712 * Connects to the host.
714 * @param {string} sharedSecret Shared secret for SPAKE negotiation.
715 * @return {void} Nothing.
718 remoting
.ClientSession
.prototype.connectToHost_ = function(sharedSecret
) {
719 this.plugin
.connect(this.hostJid
, this.hostPublicKey
, this.clientJid
,
720 sharedSecret
, this.authenticationMethods
,
725 * Callback that the plugin invokes to indicate that the connection
726 * status has changed.
729 * @param {number} status The plugin's status.
730 * @param {number} error The plugin's error state, if any.
732 remoting
.ClientSession
.prototype.onConnectionStatusUpdate_
=
733 function(status
, error
) {
734 if (status
== remoting
.ClientSession
.State
.CONNECTED
) {
735 this.setFocusHandlers_();
736 this.onDesktopSizeChanged_();
737 if (this.resizeToClient_
) {
738 this.plugin
.notifyClientResolution(window
.innerWidth
,
740 window
.devicePixelRatio
);
742 } else if (status
== remoting
.ClientSession
.State
.FAILED
) {
743 this.error_
= /** @type {remoting.ClientSession.ConnectionError} */ (error
);
745 this.setState_(/** @type {remoting.ClientSession.State} */ (status
));
749 * Callback that the plugin invokes to indicate when the connection is
753 * @param {boolean} ready True if the connection is ready.
755 remoting
.ClientSession
.prototype.onConnectionReady_ = function(ready
) {
757 this.plugin
.element().classList
.add("session-client-inactive");
759 this.plugin
.element().classList
.remove("session-client-inactive");
765 * @param {remoting.ClientSession.State} newState The new state for the session.
766 * @return {void} Nothing.
768 remoting
.ClientSession
.prototype.setState_ = function(newState
) {
769 var oldState
= this.state
;
770 this.state
= newState
;
771 var state
= this.state
;
772 if (oldState
== remoting
.ClientSession
.State
.CONNECTING
) {
773 if (this.state
== remoting
.ClientSession
.State
.CLOSED
) {
774 state
= remoting
.ClientSession
.State
.CONNECTION_CANCELED
;
775 } else if (this.state
== remoting
.ClientSession
.State
.FAILED
&&
776 this.error_
== remoting
.ClientSession
.ConnectionError
.HOST_IS_OFFLINE
&&
777 !this.logHostOfflineErrors_
) {
778 // The application requested host-offline errors to be suppressed, for
779 // example, because this connection attempt is using a cached host JID.
780 console
.log('Suppressing host-offline error.');
781 state
= remoting
.ClientSession
.State
.CONNECTION_CANCELED
;
783 } else if (oldState
== remoting
.ClientSession
.State
.CONNECTED
&&
784 this.state
== remoting
.ClientSession
.State
.FAILED
) {
785 state
= remoting
.ClientSession
.State
.CONNECTION_DROPPED
;
787 this.logToServer
.logClientSessionStateChange(state
, this.error_
, this.mode
);
788 if (this.onStateChange_
) {
789 this.onStateChange_(oldState
, newState
);
794 * This is a callback that gets called when the window is resized.
796 * @return {void} Nothing.
798 remoting
.ClientSession
.prototype.onResize = function() {
799 this.updateDimensions();
801 if (this.notifyClientResolutionTimer_
) {
802 window
.clearTimeout(this.notifyClientResolutionTimer_
);
803 this.notifyClientResolutionTimer_
= null;
806 // Defer notifying the host of the change until the window stops resizing, to
807 // avoid overloading the control channel with notifications.
808 if (this.resizeToClient_
) {
809 this.notifyClientResolutionTimer_
= window
.setTimeout(
810 this.plugin
.notifyClientResolution
.bind(this.plugin
,
813 window
.devicePixelRatio
),
817 // If bump-scrolling is enabled, adjust the plugin margins to fully utilize
818 // the new window area.
823 * Requests that the host pause or resume video updates.
825 * @param {boolean} pause True to pause video, false to resume.
826 * @return {void} Nothing.
828 remoting
.ClientSession
.prototype.pauseVideo = function(pause
) {
830 this.plugin
.pauseVideo(pause
)
835 * Requests that the host pause or resume audio.
837 * @param {boolean} pause True to pause audio, false to resume.
838 * @return {void} Nothing.
840 remoting
.ClientSession
.prototype.pauseAudio = function(pause
) {
842 this.plugin
.pauseAudio(pause
)
847 * This is a callback that gets called when the plugin notifies us of a change
848 * in the size of the remote desktop.
851 * @return {void} Nothing.
853 remoting
.ClientSession
.prototype.onDesktopSizeChanged_ = function() {
854 console
.log('desktop size changed: ' +
855 this.plugin
.desktopWidth
+ 'x' +
856 this.plugin
.desktopHeight
+' @ ' +
857 this.plugin
.desktopXDpi
+ 'x' +
858 this.plugin
.desktopYDpi
+ ' DPI');
859 this.updateDimensions();
863 * Refreshes the plugin's dimensions, taking into account the sizes of the
864 * remote desktop and client window, and the current scale-to-fit setting.
866 * @return {void} Nothing.
868 remoting
.ClientSession
.prototype.updateDimensions = function() {
869 if (this.plugin
.desktopWidth
== 0 ||
870 this.plugin
.desktopHeight
== 0) {
874 var windowWidth
= window
.innerWidth
;
875 var windowHeight
= window
.innerHeight
;
876 var desktopWidth
= this.plugin
.desktopWidth
;
877 var desktopHeight
= this.plugin
.desktopHeight
;
879 // When configured to display a host at its original size, we aim to display
880 // it as close to its physical size as possible, without losing data:
881 // - If client and host have matching DPI, render the host pixel-for-pixel.
882 // - If the host has higher DPI then still render pixel-for-pixel.
883 // - If the host has lower DPI then let Chrome up-scale it to natural size.
885 // We specify the plugin dimensions in Density-Independent Pixels, so to
886 // render pixel-for-pixel we need to down-scale the host dimensions by the
887 // devicePixelRatio of the client. To match the host pixel density, we choose
888 // an initial scale factor based on the client devicePixelRatio and host DPI.
890 // Determine the effective device pixel ratio of the host, based on DPI.
891 var hostPixelRatioX
= Math
.ceil(this.plugin
.desktopXDpi
/ 96);
892 var hostPixelRatioY
= Math
.ceil(this.plugin
.desktopYDpi
/ 96);
893 var hostPixelRatio
= Math
.min(hostPixelRatioX
, hostPixelRatioY
);
895 // Down-scale by the smaller of the client and host ratios.
896 var scale
= 1.0 / Math
.min(window
.devicePixelRatio
, hostPixelRatio
);
898 if (this.shrinkToFit_
) {
899 // Reduce the scale, if necessary, to fit the whole desktop in the window.
900 var scaleFitWidth
= Math
.min(scale
, 1.0 * windowWidth
/ desktopWidth
);
901 var scaleFitHeight
= Math
.min(scale
, 1.0 * windowHeight
/ desktopHeight
);
902 scale
= Math
.min(scaleFitHeight
, scaleFitWidth
);
904 // If we're running full-screen then try to handle common side-by-side
905 // multi-monitor combinations more intelligently.
906 if (document
.webkitIsFullScreen
) {
907 // If the host has two monitors each the same size as the client then
908 // scale-to-fit will have the desktop occupy only 50% of the client area,
909 // in which case it would be preferable to down-scale less and let the
910 // user bump-scroll around ("scale-and-pan").
911 // Triggering scale-and-pan if less than 65% of the client area would be
912 // used adds enough fuzz to cope with e.g. 1280x800 client connecting to
913 // a (2x1280)x1024 host nicely.
914 // Note that we don't need to account for scrollbars while fullscreen.
915 if (scale
<= scaleFitHeight
* 0.65) {
916 scale
= scaleFitHeight
;
918 if (scale
<= scaleFitWidth
* 0.65) {
919 scale
= scaleFitWidth
;
924 var pluginWidth
= desktopWidth
* scale
;
925 var pluginHeight
= desktopHeight
* scale
;
927 // Resize the plugin if necessary.
928 // TODO(wez): Handle high-DPI to high-DPI properly (crbug.com/135089).
929 this.plugin
.element().width
= pluginWidth
;
930 this.plugin
.element().height
= pluginHeight
;
932 // Position the container.
933 // Note that clientWidth/Height take into account scrollbars.
934 var clientWidth
= document
.documentElement
.clientWidth
;
935 var clientHeight
= document
.documentElement
.clientHeight
;
936 var parentNode
= this.plugin
.element().parentNode
;
938 if (pluginWidth
< clientWidth
) {
939 parentNode
.style
.left
= (clientWidth
- pluginWidth
) / 2 + 'px';
941 parentNode
.style
.left
= '0';
944 if (pluginHeight
< clientHeight
) {
945 parentNode
.style
.top
= (clientHeight
- pluginHeight
) / 2 + 'px';
947 parentNode
.style
.top
= '0';
950 console
.log('plugin dimensions: ' +
951 parentNode
.style
.left
+ ',' +
952 parentNode
.style
.top
+ '-' +
953 pluginWidth
+ 'x' + pluginHeight
+ '.');
957 * Returns an associative array with a set of stats for this connection.
959 * @return {remoting.ClientSession.PerfStats} The connection statistics.
961 remoting
.ClientSession
.prototype.getPerfStats = function() {
962 return this.plugin
.getPerfStats();
968 * @param {remoting.ClientSession.PerfStats} stats
970 remoting
.ClientSession
.prototype.logStatistics = function(stats
) {
971 this.logToServer
.logStatistics(stats
, this.mode
);
975 * Enable or disable logging of connection errors due to a host being offline.
976 * For example, if attempting a connection using a cached JID, host-offline
977 * errors should not be logged because the JID will be refreshed and the
978 * connection retried.
980 * @param {boolean} enable True to log host-offline errors; false to suppress.
982 remoting
.ClientSession
.prototype.logHostOfflineErrors = function(enable
) {
983 this.logHostOfflineErrors_
= enable
;
987 * Toggles between full-screen and windowed mode.
988 * @return {void} Nothing.
991 remoting
.ClientSession
.prototype.toggleFullScreen_ = function() {
992 if (document
.webkitIsFullScreen
) {
993 document
.webkitCancelFullScreen();
994 this.enableBumpScroll_(false);
996 document
.body
.webkitRequestFullScreen(Element
.ALLOW_KEYBOARD_INPUT
);
997 // Don't enable bump scrolling immediately because it can result in
998 // onMouseMove firing before the webkitIsFullScreen property can be
999 // read safely (crbug.com/132180).
1000 window
.setTimeout(this.enableBumpScroll_
.bind(this, true), 0);
1005 * Updates the options menu to reflect the current scale-to-fit and full-screen
1007 * @return {void} Nothing.
1010 remoting
.ClientSession
.prototype.onShowOptionsMenu_ = function() {
1011 remoting
.MenuButton
.select(this.resizeToClientButton_
, this.resizeToClient_
);
1012 remoting
.MenuButton
.select(this.shrinkToFitButton_
, this.shrinkToFit_
);
1013 remoting
.MenuButton
.select(this.fullScreenButton_
,
1014 document
.webkitIsFullScreen
);
1018 * Scroll the client plugin by the specified amount, keeping it visible.
1019 * Note that this is only used in content full-screen mode (not windowed or
1020 * browser full-screen modes), where window.scrollBy and the scrollTop and
1021 * scrollLeft properties don't work.
1022 * @param {number} dx The amount by which to scroll horizontally. Positive to
1023 * scroll right; negative to scroll left.
1024 * @param {number} dy The amount by which to scroll vertically. Positive to
1025 * scroll down; negative to scroll up.
1026 * @return {boolean} True if the requested scroll had no effect because both
1027 * vertical and horizontal edges of the screen have been reached.
1030 remoting
.ClientSession
.prototype.scroll_ = function(dx
, dy
) {
1031 var plugin
= this.plugin
.element();
1032 var style
= plugin
.style
;
1035 * Helper function for x- and y-scrolling
1036 * @param {number|string} curr The current margin, eg. "10px".
1037 * @param {number} delta The requested scroll amount.
1038 * @param {number} windowBound The size of the window, in pixels.
1039 * @param {number} pluginBound The size of the plugin, in pixels.
1040 * @param {{stop: boolean}} stop Reference parameter used to indicate when
1041 * the scroll has reached one of the edges and can be stopped in that
1043 * @return {string} The new margin value.
1045 var adjustMargin = function(curr
, delta
, windowBound
, pluginBound
, stop
) {
1046 var minMargin
= Math
.min(0, windowBound
- pluginBound
);
1047 var result
= (curr
? parseFloat(curr
) : 0) - delta
;
1048 result
= Math
.min(0, Math
.max(minMargin
, result
));
1049 stop
.stop
= (result
== 0 || result
== minMargin
);
1050 return result
+ "px";
1053 var stopX
= { stop
: false };
1054 style
.marginLeft
= adjustMargin(style
.marginLeft
, dx
,
1055 window
.innerWidth
, plugin
.width
, stopX
);
1056 var stopY
= { stop
: false };
1057 style
.marginTop
= adjustMargin(style
.marginTop
, dy
,
1058 window
.innerHeight
, plugin
.height
, stopY
);
1059 return stopX
.stop
&& stopY
.stop
;
1063 * Enable or disable bump-scrolling. When disabling bump scrolling, also reset
1064 * the scroll offsets to (0, 0).
1066 * @param {boolean} enable True to enable bump-scrolling, false to disable it.
1068 remoting
.ClientSession
.prototype.enableBumpScroll_ = function(enable
) {
1070 /** @type {null|function(Event):void} */
1071 this.onMouseMoveRef_
= this.onMouseMove_
.bind(this);
1072 this.plugin
.element().addEventListener('mousemove', this.onMouseMoveRef_
,
1075 this.plugin
.element().removeEventListener('mousemove', this.onMouseMoveRef_
,
1077 this.onMouseMoveRef_
= null;
1078 this.plugin
.element().style
.marginLeft
= 0;
1079 this.plugin
.element().style
.marginTop
= 0;
1084 * @param {Event} event The mouse event.
1087 remoting
.ClientSession
.prototype.onMouseMove_ = function(event
) {
1088 if (this.bumpScrollTimer_
) {
1089 window
.clearTimeout(this.bumpScrollTimer_
);
1090 this.bumpScrollTimer_
= null;
1092 // It's possible to leave content full-screen mode without using the Screen
1093 // Options menu, so we disable bump scrolling as soon as we detect this.
1094 if (!document
.webkitIsFullScreen
) {
1095 this.enableBumpScroll_(false);
1099 * Compute the scroll speed based on how close the mouse is to the edge.
1100 * @param {number} mousePos The mouse x- or y-coordinate
1101 * @param {number} size The width or height of the content area.
1102 * @return {number} The scroll delta, in pixels.
1104 var computeDelta = function(mousePos
, size
) {
1106 if (mousePos
>= size
- threshold
) {
1107 return 1 + 5 * (mousePos
- (size
- threshold
)) / threshold
;
1108 } else if (mousePos
<= threshold
) {
1109 return -1 - 5 * (threshold
- mousePos
) / threshold
;
1114 var dx
= computeDelta(event
.x
, window
.innerWidth
);
1115 var dy
= computeDelta(event
.y
, window
.innerHeight
);
1117 if (dx
!= 0 || dy
!= 0) {
1118 /** @type {remoting.ClientSession} */
1121 * Scroll the view, and schedule a timer to do so again unless we've hit
1122 * the edges of the screen. This timer is cancelled when the mouse moves.
1123 * @param {number} expected The time at which we expect to be called.
1125 var repeatScroll = function(expected
) {
1126 /** @type {number} */
1127 var now
= new Date().getTime();
1128 /** @type {number} */
1130 var lateAdjustment
= 1 + (now
- expected
) / timeout
;
1131 if (!that
.scroll_(lateAdjustment
* dx
, lateAdjustment
* dy
)) {
1132 that
.bumpScrollTimer_
= window
.setTimeout(
1133 function() { repeatScroll(now
+ timeout
); },
1137 repeatScroll(new Date().getTime());