Revert 168224 - Update V8 to version 3.15.4.
[chromium-blink-merge.git] / remoting / webapp / client_session.js
blob87151f9369f81149f5aaddd148c9c476a1d3650f
1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 /**
6 * @fileoverview
7 * Class handling creation and teardown of a remoting client session.
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
17 * differences.
20 'use strict';
22 /** @suppress {duplicate} */
23 var remoting = remoting || {};
25 /**
26 * @param {string} hostJid The jid of the host to connect to.
27 * @param {string} hostPublicKey The base64 encoded version of the host's
28 * public key.
29 * @param {string} sharedSecret The access code for IT2Me or the PIN
30 * for Me2Me.
31 * @param {string} authenticationMethods Comma-separated list of
32 * authentication methods the client should attempt to use.
33 * @param {string} authenticationTag A host-specific tag to mix into
34 * authentication hashes.
35 * @param {string} email The username for the talk network.
36 * @param {remoting.ClientSession.Mode} mode The mode of this connection.
37 * @param {function(remoting.ClientSession.State,
38 remoting.ClientSession.State):void} onStateChange
39 * The callback to invoke when the session changes state.
40 * @constructor
42 remoting.ClientSession = function(hostJid, hostPublicKey, sharedSecret,
43 authenticationMethods, authenticationTag,
44 email, mode, onStateChange) {
45 this.state = remoting.ClientSession.State.CREATED;
47 this.hostJid = hostJid;
48 this.hostPublicKey = hostPublicKey;
49 this.sharedSecret = sharedSecret;
50 this.authenticationMethods = authenticationMethods;
51 this.authenticationTag = authenticationTag;
52 this.email = email;
53 this.mode = mode;
54 this.clientJid = '';
55 this.sessionId = '';
56 /** @type {remoting.ClientPlugin} */
57 this.plugin = null;
58 this.scaleToFit = false;
59 this.hasReceivedFrame_ = false;
60 this.logToServer = new remoting.LogToServer();
61 this.onStateChange = onStateChange;
63 /** @type {number?} @private */
64 this.notifyClientDimensionsTimer_ = null;
66 /** @private */
67 this.callPluginLostFocus_ = this.pluginLostFocus_.bind(this);
68 /** @private */
69 this.callPluginGotFocus_ = this.pluginGotFocus_.bind(this);
70 /** @private */
71 this.callEnableShrink_ = this.setScaleToFit.bind(this, true);
72 /** @private */
73 this.callDisableShrink_ = this.setScaleToFit.bind(this, false);
74 /** @private */
75 this.callToggleFullScreen_ = this.toggleFullScreen_.bind(this);
76 /** @private */
77 this.screenOptionsMenu_ = new remoting.MenuButton(
78 document.getElementById('screen-options-menu'),
79 this.onShowOptionsMenu_.bind(this));
80 /** @private */
81 this.sendKeysMenu_ = new remoting.MenuButton(
82 document.getElementById('send-keys-menu')
85 /** @type {HTMLElement} @private */
86 this.shrinkToFit_ = document.getElementById('enable-shrink-to-fit');
87 /** @type {HTMLElement} @private */
88 this.originalSize_ = document.getElementById('disable-shrink-to-fit');
89 /** @type {HTMLElement} @private */
90 this.fullScreen_ = document.getElementById('toggle-full-screen');
92 this.shrinkToFit_.addEventListener('click', this.callEnableShrink_, false);
93 this.originalSize_.addEventListener('click', this.callDisableShrink_, false);
94 this.fullScreen_.addEventListener('click', this.callToggleFullScreen_, false);
95 /** @type {number?} @private */
96 this.bumpScrollTimer_ = null;
97 /**
98 * Allow error reporting to be suppressed in situations where it would not
99 * be useful, for example, when the device is offline.
101 * @type {boolean} @private
103 this.logErrors_ = true;
106 // Note that the positive values in both of these enums are copied directly
107 // from chromoting_scriptable_object.h and must be kept in sync. The negative
108 // values represent states transitions that occur within the web-app that have
109 // no corresponding plugin state transition.
110 /** @enum {number} */
111 remoting.ClientSession.State = {
112 CREATED: -3,
113 BAD_PLUGIN_VERSION: -2,
114 UNKNOWN_PLUGIN_ERROR: -1,
115 UNKNOWN: 0,
116 CONNECTING: 1,
117 INITIALIZING: 2,
118 CONNECTED: 3,
119 CLOSED: 4,
120 FAILED: 5
123 /** @enum {number} */
124 remoting.ClientSession.ConnectionError = {
125 UNKNOWN: -1,
126 NONE: 0,
127 HOST_IS_OFFLINE: 1,
128 SESSION_REJECTED: 2,
129 INCOMPATIBLE_PROTOCOL: 3,
130 NETWORK_FAILURE: 4,
131 HOST_OVERLOAD: 5
134 // The mode of this session.
135 /** @enum {number} */
136 remoting.ClientSession.Mode = {
137 IT2ME: 0,
138 ME2ME: 1
142 * Type used for performance statistics collected by the plugin.
143 * @constructor
145 remoting.ClientSession.PerfStats = function() {};
146 /** @type {number} */
147 remoting.ClientSession.PerfStats.prototype.videoBandwidth;
148 /** @type {number} */
149 remoting.ClientSession.PerfStats.prototype.videoFrameRate;
150 /** @type {number} */
151 remoting.ClientSession.PerfStats.prototype.captureLatency;
152 /** @type {number} */
153 remoting.ClientSession.PerfStats.prototype.encodeLatency;
154 /** @type {number} */
155 remoting.ClientSession.PerfStats.prototype.decodeLatency;
156 /** @type {number} */
157 remoting.ClientSession.PerfStats.prototype.renderLatency;
158 /** @type {number} */
159 remoting.ClientSession.PerfStats.prototype.roundtripLatency;
161 // Keys for connection statistics.
162 remoting.ClientSession.STATS_KEY_VIDEO_BANDWIDTH = 'videoBandwidth';
163 remoting.ClientSession.STATS_KEY_VIDEO_FRAME_RATE = 'videoFrameRate';
164 remoting.ClientSession.STATS_KEY_CAPTURE_LATENCY = 'captureLatency';
165 remoting.ClientSession.STATS_KEY_ENCODE_LATENCY = 'encodeLatency';
166 remoting.ClientSession.STATS_KEY_DECODE_LATENCY = 'decodeLatency';
167 remoting.ClientSession.STATS_KEY_RENDER_LATENCY = 'renderLatency';
168 remoting.ClientSession.STATS_KEY_ROUNDTRIP_LATENCY = 'roundtripLatency';
171 * The current state of the session.
172 * @type {remoting.ClientSession.State}
174 remoting.ClientSession.prototype.state = remoting.ClientSession.State.UNKNOWN;
177 * The last connection error. Set when state is set to FAILED.
178 * @type {remoting.ClientSession.ConnectionError}
180 remoting.ClientSession.prototype.error =
181 remoting.ClientSession.ConnectionError.NONE;
184 * The id of the client plugin
186 * @const
188 remoting.ClientSession.prototype.PLUGIN_ID = 'session-client-plugin';
191 * Callback to invoke when the state is changed.
193 * @param {remoting.ClientSession.State} oldState The previous state.
194 * @param {remoting.ClientSession.State} newState The current state.
196 remoting.ClientSession.prototype.onStateChange =
197 function(oldState, newState) { };
200 * @param {Element} container The element to add the plugin to.
201 * @param {string} id Id to use for the plugin element .
202 * @return {remoting.ClientPlugin} Create plugin object for the locally
203 * installed plugin.
205 remoting.ClientSession.prototype.createClientPlugin_ = function(container, id) {
206 var plugin = /** @type {remoting.ViewerPlugin} */
207 document.createElement('embed');
209 plugin.id = id;
210 plugin.src = 'about://none';
211 plugin.type = 'application/vnd.chromium.remoting-viewer';
212 plugin.width = 0;
213 plugin.height = 0;
214 plugin.tabIndex = 0; // Required, otherwise focus() doesn't work.
215 container.appendChild(plugin);
217 return new remoting.ClientPluginAsync(plugin);
221 * Callback function called when the plugin element gets focus.
223 remoting.ClientSession.prototype.pluginGotFocus_ = function() {
224 remoting.clipboard.initiateToHost();
228 * Callback function called when the plugin element loses focus.
230 remoting.ClientSession.prototype.pluginLostFocus_ = function() {
231 if (this.plugin) {
232 // Release all keys to prevent them becoming 'stuck down' on the host.
233 this.plugin.releaseAllKeys();
234 if (this.plugin.element()) {
235 // Focus should stay on the element, not (for example) the toolbar.
236 this.plugin.element().focus();
242 * Adds <embed> element to |container| and readies the sesion object.
244 * @param {Element} container The element to add the plugin to.
245 * @param {string} oauth2AccessToken A valid OAuth2 access token.
247 remoting.ClientSession.prototype.createPluginAndConnect =
248 function(container, oauth2AccessToken) {
249 this.plugin = this.createClientPlugin_(container, this.PLUGIN_ID);
251 this.plugin.element().focus();
253 /** @param {boolean} result */
254 this.plugin.initialize(
255 this.onPluginInitialized_.bind(this, oauth2AccessToken));
256 this.plugin.element().addEventListener(
257 'focus', this.callPluginGotFocus_, false);
258 this.plugin.element().addEventListener(
259 'blur', this.callPluginLostFocus_, false);
263 * @param {string} oauth2AccessToken
264 * @param {boolean} initialized
266 remoting.ClientSession.prototype.onPluginInitialized_ =
267 function(oauth2AccessToken, initialized) {
268 if (!initialized) {
269 console.error('ERROR: remoting plugin not loaded');
270 this.plugin.cleanup();
271 delete this.plugin;
272 this.setState_(remoting.ClientSession.State.UNKNOWN_PLUGIN_ERROR);
273 return;
276 if (!this.plugin.isSupportedVersion()) {
277 this.plugin.cleanup();
278 delete this.plugin;
279 this.setState_(remoting.ClientSession.State.BAD_PLUGIN_VERSION);
280 return;
283 // Show the Send Keys menu only if the plugin has the injectKeyEvent feature,
284 // and the Ctrl-Alt-Del button only in Me2Me mode.
285 if (!this.plugin.hasFeature(remoting.ClientPlugin.Feature.INJECT_KEY_EVENT)) {
286 var sendKeysElement = document.getElementById('send-keys-menu');
287 sendKeysElement.hidden = true;
288 } else if (this.mode != remoting.ClientSession.Mode.ME2ME) {
289 var sendCadElement = document.getElementById('send-ctrl-alt-del');
290 sendCadElement.hidden = true;
293 // Remap the right Control key to the right Win / Cmd key on ChromeOS
294 // platforms, if the plugin has the remapKey feature.
295 if (this.plugin.hasFeature(remoting.ClientPlugin.Feature.REMAP_KEY) &&
296 remoting.runningOnChromeOS()) {
297 this.plugin.remapKey(0x0700e4, 0x0700e7);
300 // Enable scale-to-fit if and only if the plugin is new enough for
301 // high-quality scaling.
302 this.setScaleToFit(this.plugin.hasFeature(
303 remoting.ClientPlugin.Feature.HIGH_QUALITY_SCALING));
305 /** @param {string} msg The IQ stanza to send. */
306 this.plugin.onOutgoingIqHandler = this.sendIq_.bind(this);
307 /** @param {string} msg The message to log. */
308 this.plugin.onDebugMessageHandler = function(msg) {
309 console.log('plugin: ' + msg);
312 this.plugin.onConnectionStatusUpdateHandler =
313 this.onConnectionStatusUpdate_.bind(this);
314 this.plugin.onConnectionReadyHandler =
315 this.onConnectionReady_.bind(this);
316 this.plugin.onDesktopSizeUpdateHandler =
317 this.onDesktopSizeChanged_.bind(this);
319 this.connectPluginToWcs_(oauth2AccessToken);
323 * Deletes the <embed> element from the container, without sending a
324 * session_terminate request. This is to be called when the session was
325 * disconnected by the Host.
327 * @return {void} Nothing.
329 remoting.ClientSession.prototype.removePlugin = function() {
330 if (this.plugin) {
331 this.plugin.element().removeEventListener(
332 'focus', this.callPluginGotFocus_, false);
333 this.plugin.element().removeEventListener(
334 'blur', this.callPluginLostFocus_, false);
335 this.plugin.cleanup();
336 this.plugin = null;
338 this.shrinkToFit_.removeEventListener('click', this.callEnableShrink_, false);
339 this.originalSize_.removeEventListener('click', this.callDisableShrink_,
340 false);
341 this.fullScreen_.removeEventListener('click', this.callToggleFullScreen_,
342 false);
346 * Deletes the <embed> element from the container and disconnects.
348 * @return {void} Nothing.
350 remoting.ClientSession.prototype.disconnect = function() {
351 // The plugin won't send a state change notification, so we explicitly log
352 // the fact that the connection has closed.
353 this.logToServer.logClientSessionStateChange(
354 remoting.ClientSession.State.CLOSED,
355 remoting.ClientSession.ConnectionError.NONE, this.mode);
356 if (remoting.wcs) {
357 remoting.wcs.setOnIq(function(stanza) {});
358 this.sendIq_(
359 '<cli:iq ' +
360 'to="' + this.hostJid + '" ' +
361 'type="set" ' +
362 'id="session-terminate" ' +
363 'xmlns:cli="jabber:client">' +
364 '<jingle ' +
365 'xmlns="urn:xmpp:jingle:1" ' +
366 'action="session-terminate" ' +
367 'initiator="' + this.clientJid + '" ' +
368 'sid="' + this.sessionId + '">' +
369 '<reason><success/></reason>' +
370 '</jingle>' +
371 '</cli:iq>');
373 this.removePlugin();
377 * Sends a key combination to the remoting client, by sending down events for
378 * the given keys, followed by up events in reverse order.
380 * @private
381 * @param {[number]} keys Key codes to be sent.
382 * @return {void} Nothing.
384 remoting.ClientSession.prototype.sendKeyCombination_ = function(keys) {
385 for (var i = 0; i < keys.length; i++) {
386 this.plugin.injectKeyEvent(keys[i], true);
388 for (var i = 0; i < keys.length; i++) {
389 this.plugin.injectKeyEvent(keys[i], false);
394 * Sends a Ctrl-Alt-Del sequence to the remoting client.
396 * @return {void} Nothing.
398 remoting.ClientSession.prototype.sendCtrlAltDel = function() {
399 this.sendKeyCombination_([0x0700e0, 0x0700e2, 0x07004c]);
403 * Sends a Print Screen keypress to the remoting client.
405 * @return {void} Nothing.
407 remoting.ClientSession.prototype.sendPrintScreen = function() {
408 this.sendKeyCombination_([0x070046]);
412 * Enables or disables the client's scale-to-fit feature.
414 * @param {boolean} scaleToFit True to enable scale-to-fit, false otherwise.
415 * @return {void} Nothing.
417 remoting.ClientSession.prototype.setScaleToFit = function(scaleToFit) {
418 this.scaleToFit = scaleToFit;
419 this.updateDimensions();
420 // If enabling scaling, reset bump-scroll offsets.
421 if (scaleToFit) {
422 this.scroll_(0, 0);
427 * Returns whether the client is currently scaling the host to fit the tab.
429 * @return {boolean} The current scale-to-fit setting.
431 remoting.ClientSession.prototype.getScaleToFit = function() {
432 return this.scaleToFit;
436 * Called when the client receives its first frame.
438 * @return {void} Nothing.
440 remoting.ClientSession.prototype.onFirstFrameReceived = function() {
441 this.hasReceivedFrame_ = true;
445 * @return {boolean} Whether the client has received a video buffer.
447 remoting.ClientSession.prototype.hasReceivedFrame = function() {
448 return this.hasReceivedFrame_;
452 * Sends an IQ stanza via the http xmpp proxy.
454 * @private
455 * @param {string} msg XML string of IQ stanza to send to server.
456 * @return {void} Nothing.
458 remoting.ClientSession.prototype.sendIq_ = function(msg) {
459 console.log(remoting.timestamp(), remoting.formatIq.prettifySendIq(msg));
460 // Extract the session id, so we can close the session later.
461 var parser = new DOMParser();
462 var iqNode = parser.parseFromString(msg, 'text/xml').firstChild;
463 var jingleNode = iqNode.firstChild;
464 if (jingleNode) {
465 var action = jingleNode.getAttribute('action');
466 if (jingleNode.nodeName == 'jingle' && action == 'session-initiate') {
467 this.sessionId = jingleNode.getAttribute('sid');
471 // Send the stanza.
472 if (remoting.wcs) {
473 remoting.wcs.sendIq(msg);
474 } else {
475 console.error('Tried to send IQ before WCS was ready.');
476 this.setState_(remoting.ClientSession.State.FAILED);
481 * Connects the plugin to WCS.
483 * @private
484 * @param {string} oauth2AccessToken A valid OAuth2 access token.
485 * @return {void} Nothing.
487 remoting.ClientSession.prototype.connectPluginToWcs_ =
488 function(oauth2AccessToken) {
489 this.clientJid = remoting.wcs.getJid();
490 if (this.clientJid == '') {
491 console.error('Tried to connect without a full JID.');
493 remoting.formatIq.setJids(this.clientJid, this.hostJid);
494 var plugin = this.plugin;
495 var forwardIq = plugin.onIncomingIq.bind(plugin);
496 /** @param {string} stanza The IQ stanza received. */
497 var onIncomingIq = function(stanza) {
498 console.log(remoting.timestamp(),
499 remoting.formatIq.prettifyReceiveIq(stanza));
500 forwardIq(stanza);
502 remoting.wcs.setOnIq(onIncomingIq);
503 this.plugin.connect(this.hostJid, this.hostPublicKey, this.clientJid,
504 this.sharedSecret, this.authenticationMethods,
505 this.authenticationTag);
509 * Callback that the plugin invokes to indicate that the connection
510 * status has changed.
512 * @private
513 * @param {number} status The plugin's status.
514 * @param {number} error The plugin's error state, if any.
516 remoting.ClientSession.prototype.onConnectionStatusUpdate_ =
517 function(status, error) {
518 if (status == remoting.ClientSession.State.CONNECTED) {
519 this.onDesktopSizeChanged_();
520 this.plugin.notifyClientDimensions(window.innerWidth, window.innerHeight)
521 } else if (status == remoting.ClientSession.State.FAILED) {
522 this.error = /** @type {remoting.ClientSession.ConnectionError} */ (error);
524 this.setState_(/** @type {remoting.ClientSession.State} */ (status));
528 * Callback that the plugin invokes to indicate when the connection is
529 * ready.
531 * @private
532 * @param {boolean} ready True if the connection is ready.
534 remoting.ClientSession.prototype.onConnectionReady_ = function(ready) {
535 if (!ready) {
536 this.plugin.element().classList.add("session-client-inactive");
537 } else {
538 this.plugin.element().classList.remove("session-client-inactive");
543 * @private
544 * @param {remoting.ClientSession.State} newState The new state for the session.
545 * @return {void} Nothing.
547 remoting.ClientSession.prototype.setState_ = function(newState) {
548 var oldState = this.state;
549 this.state = newState;
550 if (this.onStateChange) {
551 this.onStateChange(oldState, newState);
553 // If connection errors are being suppressed from the logs, translate
554 // FAILED to CLOSED here. This ensures that the duration is still logged.
555 var state = this.state;
556 if (this.state == remoting.ClientSession.State.FAILED &&
557 !this.logErrors_) {
558 console.log('Suppressing error.');
559 state = remoting.ClientSession.State.CLOSED;
561 this.logToServer.logClientSessionStateChange(state, this.error, this.mode);
565 * This is a callback that gets called when the window is resized.
567 * @return {void} Nothing.
569 remoting.ClientSession.prototype.onResize = function() {
570 this.updateDimensions();
572 if (this.notifyClientDimensionsTimer_) {
573 window.clearTimeout(this.notifyClientDimensionsTimer_);
574 this.notifyClientDimensionsTimer_ = null;
577 // Defer notifying the host of the change until the window stops resizing, to
578 // avoid overloading the control channel with notifications.
579 this.notifyClientDimensionsTimer_ = window.setTimeout(
580 this.plugin.notifyClientDimensions.bind(this.plugin,
581 window.innerWidth,
582 window.innerHeight),
583 1000);
585 // If bump-scrolling is enabled, adjust the plugin margins to fully utilize
586 // the new window area.
587 this.scroll_(0, 0);
591 * Requests that the host pause or resume video updates.
593 * @param {boolean} pause True to pause video, false to resume.
594 * @return {void} Nothing.
596 remoting.ClientSession.prototype.pauseVideo = function(pause) {
597 if (this.plugin) {
598 this.plugin.pauseVideo(pause)
603 * Requests that the host pause or resume audio.
605 * @param {boolean} pause True to pause audio, false to resume.
606 * @return {void} Nothing.
608 remoting.ClientSession.prototype.pauseAudio = function(pause) {
609 if (this.plugin) {
610 this.plugin.pauseAudio(pause)
615 * This is a callback that gets called when the plugin notifies us of a change
616 * in the size of the remote desktop.
618 * @private
619 * @return {void} Nothing.
621 remoting.ClientSession.prototype.onDesktopSizeChanged_ = function() {
622 console.log('desktop size changed: ' +
623 this.plugin.desktopWidth + 'x' +
624 this.plugin.desktopHeight +' @ ' +
625 this.plugin.desktopXDpi + 'x' +
626 this.plugin.desktopYDpi + ' DPI');
627 this.updateDimensions();
631 * Refreshes the plugin's dimensions, taking into account the sizes of the
632 * remote desktop and client window, and the current scale-to-fit setting.
634 * @return {void} Nothing.
636 remoting.ClientSession.prototype.updateDimensions = function() {
637 if (this.plugin.desktopWidth == 0 ||
638 this.plugin.desktopHeight == 0) {
639 return;
642 var windowWidth = window.innerWidth;
643 var windowHeight = window.innerHeight;
644 var desktopWidth = this.plugin.desktopWidth;
645 var desktopHeight = this.plugin.desktopHeight;
646 var scale = 1.0;
648 if (this.getScaleToFit()) {
649 // Scale to fit the entire desktop in the client window.
650 var scaleFitWidth = Math.min(1.0, 1.0 * windowWidth / desktopWidth);
651 var scaleFitHeight = Math.min(1.0, 1.0 * windowHeight / desktopHeight);
652 scale = Math.min(scaleFitHeight, scaleFitWidth);
654 // If we're running full-screen then try to handle common side-by-side
655 // multi-monitor combinations more intelligently.
656 if (document.webkitIsFullScreen) {
657 // If the host has two monitors each the same size as the client then
658 // scale-to-fit will have the desktop occupy only 50% of the client area,
659 // in which case it would be preferable to down-scale less and let the
660 // user bump-scroll around ("scale-and-pan").
661 // Triggering scale-and-pan if less than 65% of the client area would be
662 // used adds enough fuzz to cope with e.g. 1280x800 client connecting to
663 // a (2x1280)x1024 host nicely.
664 // Note that we don't need to account for scrollbars while fullscreen.
665 if (scale <= scaleFitHeight * 0.65) {
666 scale = scaleFitHeight;
668 if (scale <= scaleFitWidth * 0.65) {
669 scale = scaleFitWidth;
674 var pluginWidth = desktopWidth * scale;
675 var pluginHeight = desktopHeight * scale;
677 // Resize the plugin if necessary.
678 // TODO(wez): Handle high-DPI to high-DPI properly (crbug.com/135089).
679 this.plugin.element().width = pluginWidth;
680 this.plugin.element().height = pluginHeight;
682 // Position the container.
683 // Note that clientWidth/Height take into account scrollbars.
684 var clientWidth = document.documentElement.clientWidth;
685 var clientHeight = document.documentElement.clientHeight;
686 var parentNode = this.plugin.element().parentNode;
688 if (pluginWidth < clientWidth) {
689 parentNode.style.left = (clientWidth - pluginWidth) / 2 + 'px';
690 } else {
691 parentNode.style.left = '0';
694 if (pluginHeight < clientHeight) {
695 parentNode.style.top = (clientHeight - pluginHeight) / 2 + 'px';
696 } else {
697 parentNode.style.top = '0';
700 console.log('plugin dimensions: ' +
701 parentNode.style.left + ',' +
702 parentNode.style.top + '-' +
703 pluginWidth + 'x' + pluginHeight + '.');
707 * Returns an associative array with a set of stats for this connection.
709 * @return {remoting.ClientSession.PerfStats} The connection statistics.
711 remoting.ClientSession.prototype.getPerfStats = function() {
712 return this.plugin.getPerfStats();
716 * Logs statistics.
718 * @param {remoting.ClientSession.PerfStats} stats
720 remoting.ClientSession.prototype.logStatistics = function(stats) {
721 this.logToServer.logStatistics(stats, this.mode);
725 * Enable or disable logging of connection errors. For example, if attempting
726 * a connection using a cached JID, errors should not be logged because the
727 * JID will be refreshed and the connection retried.
729 * @param {boolean} enable True to log errors; false to suppress them.
731 remoting.ClientSession.prototype.logErrors = function(enable) {
732 this.logErrors_ = enable;
736 * Toggles between full-screen and windowed mode.
737 * @return {void} Nothing.
738 * @private
740 remoting.ClientSession.prototype.toggleFullScreen_ = function() {
741 if (document.webkitIsFullScreen) {
742 document.webkitCancelFullScreen();
743 this.enableBumpScroll_(false);
744 } else {
745 document.body.webkitRequestFullScreen(Element.ALLOW_KEYBOARD_INPUT);
746 // Don't enable bump scrolling immediately because it can result in
747 // onMouseMove firing before the webkitIsFullScreen property can be
748 // read safely (crbug.com/132180).
749 window.setTimeout(this.enableBumpScroll_.bind(this, true), 0);
754 * Updates the options menu to reflect the current scale-to-fit and full-screen
755 * settings.
756 * @return {void} Nothing.
757 * @private
759 remoting.ClientSession.prototype.onShowOptionsMenu_ = function() {
760 remoting.MenuButton.select(this.shrinkToFit_, this.scaleToFit);
761 remoting.MenuButton.select(this.originalSize_, !this.scaleToFit);
762 remoting.MenuButton.select(this.fullScreen_, document.webkitIsFullScreen);
766 * Scroll the client plugin by the specified amount, keeping it visible.
767 * Note that this is only used in content full-screen mode (not windowed or
768 * browser full-screen modes), where window.scrollBy and the scrollTop and
769 * scrollLeft properties don't work.
770 * @param {number} dx The amount by which to scroll horizontally. Positive to
771 * scroll right; negative to scroll left.
772 * @param {number} dy The amount by which to scroll vertically. Positive to
773 * scroll down; negative to scroll up.
774 * @return {boolean} True if the requested scroll had no effect because both
775 * vertical and horizontal edges of the screen have been reached.
776 * @private
778 remoting.ClientSession.prototype.scroll_ = function(dx, dy) {
779 var plugin = this.plugin.element();
780 var style = plugin.style;
783 * Helper function for x- and y-scrolling
784 * @param {number|string} curr The current margin, eg. "10px".
785 * @param {number} delta The requested scroll amount.
786 * @param {number} windowBound The size of the window, in pixels.
787 * @param {number} pluginBound The size of the plugin, in pixels.
788 * @param {{stop: boolean}} stop Reference parameter used to indicate when
789 * the scroll has reached one of the edges and can be stopped in that
790 * direction.
791 * @return {string} The new margin value.
793 var adjustMargin = function(curr, delta, windowBound, pluginBound, stop) {
794 var minMargin = Math.min(0, windowBound - pluginBound);
795 var result = (curr ? parseFloat(curr) : 0) - delta;
796 result = Math.min(0, Math.max(minMargin, result));
797 stop.stop = (result == 0 || result == minMargin);
798 return result + "px";
801 var stopX = { stop: false };
802 style.marginLeft = adjustMargin(style.marginLeft, dx,
803 window.innerWidth, plugin.width, stopX);
804 var stopY = { stop: false };
805 style.marginTop = adjustMargin(style.marginTop, dy,
806 window.innerHeight, plugin.height, stopY);
807 return stopX.stop && stopY.stop;
811 * Enable or disable bump-scrolling.
812 * @private
813 * @param {boolean} enable True to enable bump-scrolling, false to disable it.
815 remoting.ClientSession.prototype.enableBumpScroll_ = function(enable) {
816 if (enable) {
817 /** @type {null|function(Event):void} */
818 this.onMouseMoveRef_ = this.onMouseMove_.bind(this);
819 this.plugin.element().addEventListener('mousemove', this.onMouseMoveRef_,
820 false);
821 } else {
822 this.plugin.element().removeEventListener('mousemove', this.onMouseMoveRef_,
823 false);
824 this.onMouseMoveRef_ = null;
829 * @param {Event} event The mouse event.
830 * @private
832 remoting.ClientSession.prototype.onMouseMove_ = function(event) {
833 if (this.bumpScrollTimer_) {
834 window.clearTimeout(this.bumpScrollTimer_);
835 this.bumpScrollTimer_ = null;
837 // It's possible to leave content full-screen mode without using the Screen
838 // Options menu, so we disable bump scrolling as soon as we detect this.
839 if (!document.webkitIsFullScreen) {
840 this.enableBumpScroll_(false);
844 * Compute the scroll speed based on how close the mouse is to the edge.
845 * @param {number} mousePos The mouse x- or y-coordinate
846 * @param {number} size The width or height of the content area.
847 * @return {number} The scroll delta, in pixels.
849 var computeDelta = function(mousePos, size) {
850 var threshold = 10;
851 if (mousePos >= size - threshold) {
852 return 1 + 5 * (mousePos - (size - threshold)) / threshold;
853 } else if (mousePos <= threshold) {
854 return -1 - 5 * (threshold - mousePos) / threshold;
856 return 0;
859 var dx = computeDelta(event.x, window.innerWidth);
860 var dy = computeDelta(event.y, window.innerHeight);
862 if (dx != 0 || dy != 0) {
863 /** @type {remoting.ClientSession} */
864 var that = this;
866 * Scroll the view, and schedule a timer to do so again unless we've hit
867 * the edges of the screen. This timer is cancelled when the mouse moves.
868 * @param {number} expected The time at which we expect to be called.
870 var repeatScroll = function(expected) {
871 /** @type {number} */
872 var now = new Date().getTime();
873 /** @type {number} */
874 var timeout = 10;
875 var lateAdjustment = 1 + (now - expected) / timeout;
876 if (!that.scroll_(lateAdjustment * dx, lateAdjustment * dy)) {
877 that.bumpScrollTimer_ = window.setTimeout(
878 function() { repeatScroll(now + timeout); },
879 timeout);
882 repeatScroll(new Date().getTime());