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. 4 5 /** 6 * @fileoverview 7 * Class handling creation and teardown of a remoting client session. 8 * 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, 14 * 15 * This class should not access the plugin directly, instead it should 16 * do it through ClientPlugin class which abstracts plugin version 17 * differences. 18 */ 19 20 'use strict'; 21 22 /** @suppress {duplicate} */ 23 var remoting = remoting || {}; 24 25 /** 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 29 * interactively. 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 40 * public key. 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. 46 * @constructor 47 */ 48 remoting.ClientSession = function(accessCode, fetchPin, fetchThirdPartyToken, 49 authenticationMethods, 50 hostId, hostJid, hostPublicKey, mode, 51 clientPairingId, clientPairedSecret) { 52 /** @private */ 53 this.state_ = remoting.ClientSession.State.CREATED; 54 55 /** @private */ 56 this.error_ = remoting.Error.NONE; 57 58 /** @private */ 59 this.hostJid_ = hostJid; 60 /** @private */ 61 this.hostPublicKey_ = hostPublicKey; 62 /** @private */ 63 this.accessCode_ = accessCode; 64 /** @private */ 65 this.fetchPin_ = fetchPin; 66 /** @private */ 67 this.fetchThirdPartyToken_ = fetchThirdPartyToken; 68 /** @private */ 69 this.authenticationMethods_ = authenticationMethods; 70 /** @private */ 71 this.hostId_ = hostId; 72 /** @private */ 73 this.mode_ = mode; 74 /** @private */ 75 this.clientPairingId_ = clientPairingId; 76 /** @private */ 77 this.clientPairedSecret_ = clientPairedSecret; 78 /** @private */ 79 this.sessionId_ = ''; 80 /** @type {remoting.ClientPlugin} 81 * @private */ 82 this.plugin_ = null; 83 /** @private */ 84 this.shrinkToFit_ = true; 85 /** @private */ 86 this.resizeToClient_ = true; 87 /** @private */ 88 this.remapKeys_ = ''; 89 /** @private */ 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; 95 96 /** @type {number?} @private */ 97 this.notifyClientResolutionTimer_ = null; 98 /** @type {number?} @private */ 99 this.bumpScrollTimer_ = null; 100 101 /** 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. 104 * 105 * @type {boolean} @private 106 */ 107 this.logHostOfflineErrors_ = true; 108 109 /** @private */ 110 this.callPluginLostFocus_ = this.pluginLostFocus_.bind(this); 111 /** @private */ 112 this.callPluginGotFocus_ = this.pluginGotFocus_.bind(this); 113 /** @private */ 114 this.callSetScreenMode_ = this.onSetScreenMode_.bind(this); 115 /** @private */ 116 this.callToggleFullScreen_ = this.toggleFullScreen_.bind(this); 117 118 /** @private */ 119 this.screenOptionsMenu_ = new remoting.MenuButton( 120 document.getElementById('screen-options-menu'), 121 this.onShowOptionsMenu_.bind(this)); 122 /** @private */ 123 this.sendKeysMenu_ = new remoting.MenuButton( 124 document.getElementById('send-keys-menu') 125 ); 126 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'); 134 135 if (this.mode_ == remoting.ClientSession.Mode.IT2ME) { 136 // Resize-to-client is not supported for IT2Me hosts. 137 this.resizeToClientButton_.hidden = true; 138 } else { 139 this.resizeToClientButton_.hidden = false; 140 this.resizeToClientButton_.addEventListener( 141 'click', this.callSetScreenMode_, false); 142 } 143 144 this.shrinkToFitButton_.addEventListener( 145 'click', this.callSetScreenMode_, false); 146 this.fullScreenButton_.addEventListener( 147 'click', this.callToggleFullScreen_, false); 148 }; 149 150 /** 151 * @param {?function(remoting.ClientSession.State, 152 remoting.ClientSession.State):void} onStateChange 153 * The callback to invoke when the session changes state. 154 */ 155 remoting.ClientSession.prototype.setOnStateChange = function(onStateChange) { 156 this.onStateChange_ = onStateChange; 157 }; 158 159 /** 160 * Called when the window or desktop size or the scaling settings change, 161 * to set the scroll-bar visibility. 162 * 163 * TODO(jamiewalch): crbug.com/252796: Remove this once crbug.com/240772 is 164 * fixed. 165 */ 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; 181 } 182 } 183 184 var htmlNode = /** @type {HTMLElement} */ (document.body.parentNode); 185 if (needsHorizontalScroll) { 186 htmlNode.classList.remove('no-horizontal-scroll'); 187 } else { 188 htmlNode.classList.add('no-horizontal-scroll'); 189 } 190 if (needsVerticalScroll) { 191 htmlNode.classList.remove('no-vertical-scroll'); 192 } else { 193 htmlNode.classList.add('no-vertical-scroll'); 194 } 195 }; 196 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. 205 CREATED: -1, 206 UNKNOWN: 0, 207 CONNECTING: 1, 208 INITIALIZING: 2, 209 CONNECTED: 3, 210 CLOSED: 4, 211 FAILED: 5 212 }; 213 214 /** @enum {number} */ 215 remoting.ClientSession.ConnectionError = { 216 UNKNOWN: -1, 217 NONE: 0, 218 HOST_IS_OFFLINE: 1, 219 SESSION_REJECTED: 2, 220 INCOMPATIBLE_PROTOCOL: 3, 221 NETWORK_FAILURE: 4, 222 HOST_OVERLOAD: 5 223 }; 224 225 // The mode of this session. 226 /** @enum {number} */ 227 remoting.ClientSession.Mode = { 228 IT2ME: 0, 229 ME2ME: 1 230 }; 231 232 /** 233 * Type used for performance statistics collected by the plugin. 234 * @constructor 235 */ 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; 251 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'; 260 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'; 265 266 /** 267 * The id of the client plugin 268 * 269 * @const 270 */ 271 remoting.ClientSession.prototype.PLUGIN_ID = 'session-client-plugin'; 272 273 /** 274 * Set of capabilities for which hasCapability_() can be used to test. 275 * 276 * @enum {string} 277 */ 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' 284 }; 285 286 /** 287 * The set of capabilities negotiated between the client and host. 288 * @type {Array.<string>} 289 * @private 290 */ 291 remoting.ClientSession.prototype.capabilities_ = null; 292 293 /** 294 * @param {remoting.ClientSession.Capability} capability The capability to test 295 * for. 296 * @return {boolean} True if the capability has been negotiated between 297 * the client and host. 298 * @private 299 */ 300 remoting.ClientSession.prototype.hasCapability_ = function(capability) { 301 if (this.capabilities_ == null) 302 return false; 303 304 return this.capabilities_.indexOf(capability) > -1; 305 }; 306 307 /** 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 311 * installed plugin. 312 */ 313 remoting.ClientSession.prototype.createClientPlugin_ = function(container, id) { 314 var plugin = /** @type {remoting.ViewerPlugin} */ 315 document.createElement('embed'); 316 317 plugin.id = id; 318 plugin.src = 'about://none'; 319 plugin.type = 'application/vnd.chromium.remoting-viewer'; 320 plugin.width = 0; 321 plugin.height = 0; 322 plugin.tabIndex = 0; // Required, otherwise focus() doesn't work. 323 container.appendChild(plugin); 324 325 return new remoting.ClientPluginAsync(plugin); 326 }; 327 328 /** 329 * Callback function called when the plugin element gets focus. 330 */ 331 remoting.ClientSession.prototype.pluginGotFocus_ = function() { 332 remoting.clipboard.initiateToHost(); 333 }; 334 335 /** 336 * Callback function called when the plugin element loses focus. 337 */ 338 remoting.ClientSession.prototype.pluginLostFocus_ = function() { 339 if (this.plugin_) { 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(); 345 } 346 } 347 }; 348 349 /** 350 * Adds <embed> element to |container| and readies the sesion object. 351 * 352 * @param {Element} container The element to add the plugin to. 353 */ 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)); 359 }; 360 361 /** 362 * @param {Object.<string>} options The current options for the host, or {} 363 * if this client has no saved settings for the host. 364 * @private 365 */ 366 remoting.ClientSession.prototype.onHostSettingsLoaded_ = function(options) { 367 if (remoting.ClientSession.KEY_REMAP_KEYS in options && 368 typeof(options[remoting.ClientSession.KEY_REMAP_KEYS]) == 369 'string') { 370 this.remapKeys_ = /** @type {string} */ 371 options[remoting.ClientSession.KEY_REMAP_KEYS]; 372 } 373 if (remoting.ClientSession.KEY_RESIZE_TO_CLIENT in options && 374 typeof(options[remoting.ClientSession.KEY_RESIZE_TO_CLIENT]) == 375 'boolean') { 376 this.resizeToClient_ = /** @type {boolean} */ 377 options[remoting.ClientSession.KEY_RESIZE_TO_CLIENT]; 378 } 379 if (remoting.ClientSession.KEY_SHRINK_TO_FIT in options && 380 typeof(options[remoting.ClientSession.KEY_SHRINK_TO_FIT]) == 381 'boolean') { 382 this.shrinkToFit_ = /** @type {boolean} */ 383 options[remoting.ClientSession.KEY_SHRINK_TO_FIT]; 384 } 385 386 /** @param {boolean} result */ 387 this.plugin_.initialize(this.onPluginInitialized_.bind(this)); 388 }; 389 390 /** 391 * Constrains the focus to the plugin element. 392 * @private 393 */ 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(); 400 }; 401 402 /** 403 * @param {remoting.Error} error 404 */ 405 remoting.ClientSession.prototype.resetWithError_ = function(error) { 406 this.plugin_.cleanup(); 407 delete this.plugin_; 408 this.error_ = error; 409 this.setState_(remoting.ClientSession.State.FAILED); 410 } 411 412 /** 413 * @param {boolean} initialized 414 */ 415 remoting.ClientSession.prototype.onPluginInitialized_ = function(initialized) { 416 if (!initialized) { 417 console.error('ERROR: remoting plugin not loaded'); 418 this.resetWithError_(remoting.Error.MISSING_PLUGIN); 419 return; 420 } 421 422 if (!this.plugin_.isSupportedVersion()) { 423 this.resetWithError_(remoting.Error.BAD_PLUGIN_VERSION); 424 return; 425 } 426 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; 436 } 437 438 // Apply customized key remappings if the plugin supports remapKeys. 439 if (this.plugin_.hasFeature(remoting.ClientPlugin.Feature.REMAP_KEY)) { 440 this.applyRemapKeys_(true); 441 } 442 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); 448 }; 449 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_(); 459 }; 460 461 /** 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. 465 * 466 * @return {void} Nothing. 467 */ 468 remoting.ClientSession.prototype.removePlugin = function() { 469 if (this.plugin_) { 470 this.plugin_.element().removeEventListener( 471 'focus', this.callPluginGotFocus_, false); 472 this.plugin_.element().removeEventListener( 473 'blur', this.callPluginLostFocus_, false); 474 this.plugin_.cleanup(); 475 this.plugin_ = null; 476 } 477 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); 485 486 // In case the user had selected full-screen mode, cancel it now. 487 document.webkitCancelFullScreen(); 488 }; 489 490 /** 491 * Deletes the <embed> element from the container and disconnects. 492 * 493 * @param {boolean} isUserInitiated True for user-initiated disconnects, False 494 * for disconnects due to connection failures. 495 * @return {void} Nothing. 496 */ 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_); 503 } 504 remoting.wcsSandbox.setOnIq(null); 505 this.sendIq_( 506 '<cli:iq ' + 507 'to="' + this.hostJid_ + '" ' + 508 'type="set" ' + 509 'id="session-terminate" ' + 510 'xmlns:cli="jabber:client">' + 511 '<jingle ' + 512 'xmlns="urn:xmpp:jingle:1" ' + 513 'action="session-terminate" ' + 514 'sid="' + this.sessionId_ + '">' + 515 '<reason><success/></reason>' + 516 '</jingle>' + 517 '</cli:iq>'); 518 this.removePlugin(); 519 }; 520 521 /** 522 * @return {remoting.ClientSession.Mode} The current state. 523 */ 524 remoting.ClientSession.prototype.getMode = function() { 525 return this.mode_; 526 }; 527 528 /** 529 * @return {remoting.ClientSession.State} The current state. 530 */ 531 remoting.ClientSession.prototype.getState = function() { 532 return this.state_; 533 }; 534 535 /** 536 * @return {remoting.Error} The current error code. 537 */ 538 remoting.ClientSession.prototype.getError = function() { 539 return this.error_; 540 }; 541 542 /** 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. 545 * 546 * @private 547 * @param {[number]} keys Key codes to be sent. 548 * @return {void} Nothing. 549 */ 550 remoting.ClientSession.prototype.sendKeyCombination_ = function(keys) { 551 for (var i = 0; i < keys.length; i++) { 552 this.plugin_.injectKeyEvent(keys[i], true); 553 } 554 for (var i = 0; i < keys.length; i++) { 555 this.plugin_.injectKeyEvent(keys[i], false); 556 } 557 } 558 559 /** 560 * Sends a Ctrl-Alt-Del sequence to the remoting client. 561 * 562 * @return {void} Nothing. 563 */ 564 remoting.ClientSession.prototype.sendCtrlAltDel = function() { 565 this.sendKeyCombination_([0x0700e0, 0x0700e2, 0x07004c]); 566 } 567 568 /** 569 * Sends a Print Screen keypress to the remoting client. 570 * 571 * @return {void} Nothing. 572 */ 573 remoting.ClientSession.prototype.sendPrintScreen = function() { 574 this.sendKeyCombination_([0x070046]); 575 } 576 577 /** 578 * Sets and stores the key remapping setting for the current host. 579 * 580 * @param {string} remappings Comma separated list of key remappings. 581 */ 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); 587 588 // Save the new remapping setting. 589 var options = {}; 590 options[remoting.ClientSession.KEY_REMAP_KEYS] = this.remapKeys_; 591 remoting.HostSettings.save(this.hostId_, options); 592 } 593 594 /** 595 * Applies the configured key remappings to the session, or resets them. 596 * 597 * @param {boolean} apply True to apply remappings, false to cancel them. 598 */ 599 remoting.ClientSession.prototype.applyRemapKeys_ = function(apply) { 600 // By default, under ChromeOS, remap the right Control key to the right 601 // Win / Cmd key. 602 var remapKeys = this.remapKeys_; 603 if (remapKeys == '' && remoting.runningOnChromeOS()) { 604 remapKeys = '0x0700e4>0x0700e7'; 605 } 606 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]); 612 continue; 613 } 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]); 618 continue; 619 } 620 if (apply) { 621 console.log('remapKey 0x' + fromKey.toString(16) + 622 '>0x' + toKey.toString(16)); 623 this.plugin_.remapKey(fromKey, toKey); 624 } else { 625 console.log('cancel remapKey 0x' + fromKey.toString(16)); 626 this.plugin_.remapKey(fromKey, fromKey); 627 } 628 } 629 } 630 631 /** 632 * Callback for the two "screen mode" related menu items: Resize desktop to 633 * fit and Shrink to fit. 634 * 635 * @param {Event} event The click event indicating which mode was selected. 636 * @return {void} Nothing. 637 * @private 638 */ 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; 644 } 645 if (event.target == this.resizeToClientButton_) { 646 resizeToClient = !resizeToClient; 647 } 648 this.setScreenMode_(shrinkToFit, resizeToClient); 649 }; 650 651 /** 652 * Set the shrink-to-fit and resize-to-client flags and save them if this is 653 * a Me2Me connection. 654 * 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. 663 * @private 664 */ 665 remoting.ClientSession.prototype.setScreenMode_ = 666 function(shrinkToFit, resizeToClient) { 667 if (resizeToClient && !this.resizeToClient_) { 668 this.plugin_.notifyClientResolution(window.innerWidth, 669 window.innerHeight, 670 window.devicePixelRatio); 671 } 672 673 // If enabling shrink, reset bump-scroll offsets. 674 var needsScrollReset = shrinkToFit && !this.shrinkToFit_; 675 676 this.shrinkToFit_ = shrinkToFit; 677 this.resizeToClient_ = resizeToClient; 678 this.updateScrollbarVisibility(); 679 680 if (this.hostId_ != '') { 681 var options = {}; 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); 685 } 686 687 this.updateDimensions(); 688 if (needsScrollReset) { 689 this.scroll_(0, 0); 690 } 691 692 } 693 694 /** 695 * Called when the client receives its first frame. 696 * 697 * @return {void} Nothing. 698 */ 699 remoting.ClientSession.prototype.onFirstFrameReceived = function() { 700 this.hasReceivedFrame_ = true; 701 }; 702 703 /** 704 * @return {boolean} Whether the client has received a video buffer. 705 */ 706 remoting.ClientSession.prototype.hasReceivedFrame = function() { 707 return this.hasReceivedFrame_; 708 }; 709 710 /** 711 * Sends an IQ stanza via the http xmpp proxy. 712 * 713 * @private 714 * @param {string} msg XML string of IQ stanza to send to server. 715 * @return {void} Nothing. 716 */ 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; 722 if (jingleNode) { 723 var action = jingleNode.getAttribute('action'); 724 if (jingleNode.nodeName == 'jingle' && action == 'session-initiate') { 725 this.sessionId_ = jingleNode.getAttribute('sid'); 726 } 727 } 728 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'); 734 if (type == 'set') { 735 var id = iqNode.getAttribute('id'); 736 iqNode.setAttribute('id', 'x' + id); 737 msg = (new XMLSerializer()).serializeToString(iqNode); 738 } 739 740 console.log(remoting.timestamp(), remoting.formatIq.prettifySendIq(msg)); 741 742 // Send the stanza. 743 remoting.wcsSandbox.sendIq(msg); 744 }; 745 746 remoting.ClientSession.prototype.initiateConnection_ = function() { 747 /** @type {remoting.ClientSession} */ 748 var that = this; 749 750 remoting.wcsSandbox.connect(onWcsConnected, this.resetWithError_.bind(this)); 751 752 /** @param {string} localJid Local JID. */ 753 function onWcsConnected(localJid) { 754 that.connectPluginToWcs_(localJid); 755 that.getSharedSecret_(onSharedSecretReceived.bind(null, localJid)); 756 } 757 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_); 765 }; 766 } 767 768 /** 769 * Connects the plugin to WCS. 770 * 771 * @private 772 * @param {string} localJid Local JID. 773 * @return {void} Nothing. 774 */ 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_(). 781 try { 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); 789 } 790 } catch (err) { 791 // Pass message as is when it is malformed. 792 } 793 794 console.log(remoting.timestamp(), 795 remoting.formatIq.prettifyReceiveIq(stanza)); 796 forwardIq(stanza); 797 }; 798 remoting.wcsSandbox.setOnIq(onIncomingIq); 799 } 800 801 /** 802 * Gets shared secret to be used for connection. 803 * 804 * @param {function(string)} callback Callback called with the shared secret. 805 * @return {void} Nothing. 806 * @private 807 */ 808 remoting.ClientSession.prototype.getSharedSecret_ = function(callback) { 809 /** @type remoting.ClientSession */ 810 var that = this; 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_)); 817 }; 818 this.plugin_.fetchThirdPartyTokenHandler = fetchThirdPartyToken; 819 } 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_)); 831 }; 832 this.plugin_.fetchPinHandler = fetchPin; 833 callback(''); 834 } else { 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); 838 } 839 }; 840 841 /** 842 * Callback that the plugin invokes to indicate that the connection 843 * status has changed. 844 * 845 * @private 846 * @param {number} status The plugin's status. 847 * @param {number} error The plugin's error state, if any. 848 */ 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, 856 window.innerHeight, 857 window.devicePixelRatio); 858 } 859 } else if (status == remoting.ClientSession.State.FAILED) { 860 switch (error) { 861 case remoting.ClientSession.ConnectionError.HOST_IS_OFFLINE: 862 this.error_ = remoting.Error.HOST_IS_OFFLINE; 863 break; 864 case remoting.ClientSession.ConnectionError.SESSION_REJECTED: 865 this.error_ = remoting.Error.INVALID_ACCESS_CODE; 866 break; 867 case remoting.ClientSession.ConnectionError.INCOMPATIBLE_PROTOCOL: 868 this.error_ = remoting.Error.INCOMPATIBLE_PROTOCOL; 869 break; 870 case remoting.ClientSession.ConnectionError.NETWORK_FAILURE: 871 this.error_ = remoting.Error.P2P_FAILURE; 872 break; 873 case remoting.ClientSession.ConnectionError.HOST_OVERLOAD: 874 this.error_ = remoting.Error.HOST_OVERLOAD; 875 break; 876 default: 877 this.error_ = remoting.Error.UNEXPECTED; 878 } 879 } 880 this.setState_(/** @type {remoting.ClientSession.State} */ (status)); 881 }; 882 883 /** 884 * Callback that the plugin invokes to indicate when the connection is 885 * ready. 886 * 887 * @private 888 * @param {boolean} ready True if the connection is ready. 889 */ 890 remoting.ClientSession.prototype.onConnectionReady_ = function(ready) { 891 if (!ready) { 892 this.plugin_.element().classList.add("session-client-inactive"); 893 } else { 894 this.plugin_.element().classList.remove("session-client-inactive"); 895 } 896 }; 897 898 /** 899 * Called when the client-host capabilities negotiation is complete. 900 * 901 * @param {!Array.<string>} capabilities The set of capabilities negotiated 902 * between the client and host. 903 * @return {void} Nothing. 904 * @private 905 */ 906 remoting.ClientSession.prototype.onSetCapabilities_ = function(capabilities) { 907 if (this.capabilities_ != null) { 908 console.error('onSetCapabilities_() is called more than once'); 909 return; 910 } 911 912 this.capabilities_ = capabilities; 913 if (this.hasCapability_( 914 remoting.ClientSession.Capability.SEND_INITIAL_RESOLUTION)) { 915 this.plugin_.notifyClientResolution(window.innerWidth, 916 window.innerHeight, 917 window.devicePixelRatio); 918 } 919 }; 920 921 /** 922 * @private 923 * @param {remoting.ClientSession.State} newState The new state for the session. 924 * @return {void} Nothing. 925 */ 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; 940 } 941 } else if (oldState == remoting.ClientSession.State.CONNECTED && 942 this.state_ == remoting.ClientSession.State.FAILED) { 943 state = remoting.ClientSession.State.CONNECTION_DROPPED; 944 } 945 this.logToServer.logClientSessionStateChange(state, this.error_, this.mode_); 946 if (this.onStateChange_) { 947 this.onStateChange_(oldState, newState); 948 } 949 }; 950 951 /** 952 * This is a callback that gets called when the window is resized. 953 * 954 * @return {void} Nothing. 955 */ 956 remoting.ClientSession.prototype.onResize = function() { 957 this.updateDimensions(); 958 959 if (this.notifyClientResolutionTimer_) { 960 window.clearTimeout(this.notifyClientResolutionTimer_); 961 this.notifyClientResolutionTimer_ = null; 962 } 963 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; 971 } 972 this.notifyClientResolutionTimer_ = window.setTimeout( 973 this.plugin_.notifyClientResolution.bind(this.plugin_, 974 window.innerWidth, 975 window.innerHeight, 976 window.devicePixelRatio), 977 kResizeRateLimitMs); 978 } 979 980 // If bump-scrolling is enabled, adjust the plugin margins to fully utilize 981 // the new window area. 982 this.scroll_(0, 0); 983 984 this.updateScrollbarVisibility(); 985 }; 986 987 /** 988 * Requests that the host pause or resume video updates. 989 * 990 * @param {boolean} pause True to pause video, false to resume. 991 * @return {void} Nothing. 992 */ 993 remoting.ClientSession.prototype.pauseVideo = function(pause) { 994 if (this.plugin_) { 995 this.plugin_.pauseVideo(pause) 996 } 997 } 998 999 /** 1000 * Requests that the host pause or resume audio. 1001 * 1002 * @param {boolean} pause True to pause audio, false to resume. 1003 * @return {void} Nothing. 1004 */ 1005 remoting.ClientSession.prototype.pauseAudio = function(pause) { 1006 if (this.plugin_) { 1007 this.plugin_.pauseAudio(pause) 1008 } 1009 } 1010 1011 /** 1012 * This is a callback that gets called when the plugin notifies us of a change 1013 * in the size of the remote desktop. 1014 * 1015 * @private 1016 * @return {void} Nothing. 1017 */ 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(); 1026 }; 1027 1028 /** 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. 1031 * 1032 * @return {void} Nothing. 1033 */ 1034 remoting.ClientSession.prototype.updateDimensions = function() { 1035 if (this.plugin_.desktopWidth == 0 || 1036 this.plugin_.desktopHeight == 0) { 1037 return; 1038 } 1039 1040 var windowWidth = window.innerWidth; 1041 var windowHeight = window.innerHeight; 1042 var desktopWidth = this.plugin_.desktopWidth; 1043 var desktopHeight = this.plugin_.desktopHeight; 1044 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. 1050 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. 1055 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); 1060 1061 // Down-scale by the smaller of the client and host ratios. 1062 var scale = 1.0 / Math.min(window.devicePixelRatio, hostPixelRatio); 1063 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); 1069 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; 1083 } 1084 if (scale <= scaleFitWidth * 0.65) { 1085 scale = scaleFitWidth; 1086 } 1087 } 1088 } 1089 1090 var pluginWidth = desktopWidth * scale; 1091 var pluginHeight = desktopHeight * scale; 1092 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; 1097 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; 1103 1104 if (pluginWidth < clientWidth) { 1105 parentNode.style.left = (clientWidth - pluginWidth) / 2 + 'px'; 1106 } else { 1107 parentNode.style.left = '0'; 1108 } 1109 1110 if (pluginHeight < clientHeight) { 1111 parentNode.style.top = (clientHeight - pluginHeight) / 2 + 'px'; 1112 } else { 1113 parentNode.style.top = '0'; 1114 } 1115 1116 console.log('plugin dimensions: ' + 1117 parentNode.style.left + ',' + 1118 parentNode.style.top + '-' + 1119 pluginWidth + 'x' + pluginHeight + '.'); 1120 }; 1121 1122 /** 1123 * Returns an associative array with a set of stats for this connection. 1124 * 1125 * @return {remoting.ClientSession.PerfStats} The connection statistics. 1126 */ 1127 remoting.ClientSession.prototype.getPerfStats = function() { 1128 return this.plugin_.getPerfStats(); 1129 }; 1130 1131 /** 1132 * Logs statistics. 1133 * 1134 * @param {remoting.ClientSession.PerfStats} stats 1135 */ 1136 remoting.ClientSession.prototype.logStatistics = function(stats) { 1137 this.logToServer.logStatistics(stats, this.mode_); 1138 }; 1139 1140 /** 1141 * Enable or disable logging of connection errors due to a host being offline. 1142 * For example, if attempting a connection using a cached JID, host-offline 1143 * errors should not be logged because the JID will be refreshed and the 1144 * connection retried. 1145 * 1146 * @param {boolean} enable True to log host-offline errors; false to suppress. 1147 */ 1148 remoting.ClientSession.prototype.logHostOfflineErrors = function(enable) { 1149 this.logHostOfflineErrors_ = enable; 1150 }; 1151 1152 /** 1153 * Request pairing with the host for PIN-less authentication. 1154 * 1155 * @param {string} clientName The human-readable name of the client. 1156 * @param {function(string, string):void} onDone Callback to receive the 1157 * client id and shared secret when they are available. 1158 */ 1159 remoting.ClientSession.prototype.requestPairing = function(clientName, onDone) { 1160 if (this.plugin_) { 1161 this.plugin_.requestPairing(clientName, onDone); 1162 } 1163 }; 1164 1165 /** 1166 * Toggles between full-screen and windowed mode. 1167 * @return {void} Nothing. 1168 * @private 1169 */ 1170 remoting.ClientSession.prototype.toggleFullScreen_ = function() { 1171 if (document.webkitIsFullScreen) { 1172 document.webkitCancelFullScreen(); 1173 this.enableBumpScroll_(false); 1174 } else { 1175 document.body.webkitRequestFullScreen(Element.ALLOW_KEYBOARD_INPUT); 1176 // Don't enable bump scrolling immediately because it can result in 1177 // onMouseMove firing before the webkitIsFullScreen property can be 1178 // read safely (crbug.com/132180). 1179 window.setTimeout(this.enableBumpScroll_.bind(this, true), 0); 1180 } 1181 }; 1182 1183 /** 1184 * Updates the options menu to reflect the current scale-to-fit and full-screen 1185 * settings. 1186 * @return {void} Nothing. 1187 * @private 1188 */ 1189 remoting.ClientSession.prototype.onShowOptionsMenu_ = function() { 1190 remoting.MenuButton.select(this.resizeToClientButton_, this.resizeToClient_); 1191 remoting.MenuButton.select(this.shrinkToFitButton_, this.shrinkToFit_); 1192 remoting.MenuButton.select(this.fullScreenButton_, 1193 document.webkitIsFullScreen); 1194 }; 1195 1196 /** 1197 * Scroll the client plugin by the specified amount, keeping it visible. 1198 * Note that this is only used in content full-screen mode (not windowed or 1199 * browser full-screen modes), where window.scrollBy and the scrollTop and 1200 * scrollLeft properties don't work. 1201 * @param {number} dx The amount by which to scroll horizontally. Positive to 1202 * scroll right; negative to scroll left. 1203 * @param {number} dy The amount by which to scroll vertically. Positive to 1204 * scroll down; negative to scroll up. 1205 * @return {boolean} True if the requested scroll had no effect because both 1206 * vertical and horizontal edges of the screen have been reached. 1207 * @private 1208 */ 1209 remoting.ClientSession.prototype.scroll_ = function(dx, dy) { 1210 var plugin = this.plugin_.element(); 1211 var style = plugin.style; 1212 1213 /** 1214 * Helper function for x- and y-scrolling 1215 * @param {number|string} curr The current margin, eg. "10px". 1216 * @param {number} delta The requested scroll amount. 1217 * @param {number} windowBound The size of the window, in pixels. 1218 * @param {number} pluginBound The size of the plugin, in pixels. 1219 * @param {{stop: boolean}} stop Reference parameter used to indicate when 1220 * the scroll has reached one of the edges and can be stopped in that 1221 * direction. 1222 * @return {string} The new margin value. 1223 */ 1224 var adjustMargin = function(curr, delta, windowBound, pluginBound, stop) { 1225 var minMargin = Math.min(0, windowBound - pluginBound); 1226 var result = (curr ? parseFloat(curr) : 0) - delta; 1227 result = Math.min(0, Math.max(minMargin, result)); 1228 stop.stop = (result == 0 || result == minMargin); 1229 return result + "px"; 1230 }; 1231 1232 var stopX = { stop: false }; 1233 style.marginLeft = adjustMargin(style.marginLeft, dx, 1234 window.innerWidth, plugin.width, stopX); 1235 var stopY = { stop: false }; 1236 style.marginTop = adjustMargin(style.marginTop, dy, 1237 window.innerHeight, plugin.height, stopY); 1238 return stopX.stop && stopY.stop; 1239 } 1240 1241 /** 1242 * Enable or disable bump-scrolling. When disabling bump scrolling, also reset 1243 * the scroll offsets to (0, 0). 1244 * @private 1245 * @param {boolean} enable True to enable bump-scrolling, false to disable it. 1246 */ 1247 remoting.ClientSession.prototype.enableBumpScroll_ = function(enable) { 1248 if (enable) { 1249 /** @type {null|function(Event):void} */ 1250 this.onMouseMoveRef_ = this.onMouseMove_.bind(this); 1251 this.plugin_.element().addEventListener( 1252 'mousemove', this.onMouseMoveRef_, false); 1253 } else { 1254 this.plugin_.element().removeEventListener( 1255 'mousemove', this.onMouseMoveRef_, false); 1256 this.onMouseMoveRef_ = null; 1257 this.plugin_.element().style.marginLeft = 0; 1258 this.plugin_.element().style.marginTop = 0; 1259 } 1260 }; 1261 1262 /** 1263 * @param {Event} event The mouse event. 1264 * @private 1265 */ 1266 remoting.ClientSession.prototype.onMouseMove_ = function(event) { 1267 if (this.bumpScrollTimer_) { 1268 window.clearTimeout(this.bumpScrollTimer_); 1269 this.bumpScrollTimer_ = null; 1270 } 1271 // It's possible to leave content full-screen mode without using the Screen 1272 // Options menu, so we disable bump scrolling as soon as we detect this. 1273 if (!document.webkitIsFullScreen) { 1274 this.enableBumpScroll_(false); 1275 } 1276 1277 /** 1278 * Compute the scroll speed based on how close the mouse is to the edge. 1279 * @param {number} mousePos The mouse x- or y-coordinate 1280 * @param {number} size The width or height of the content area. 1281 * @return {number} The scroll delta, in pixels. 1282 */ 1283 var computeDelta = function(mousePos, size) { 1284 var threshold = 10; 1285 if (mousePos >= size - threshold) { 1286 return 1 + 5 * (mousePos - (size - threshold)) / threshold; 1287 } else if (mousePos <= threshold) { 1288 return -1 - 5 * (threshold - mousePos) / threshold; 1289 } 1290 return 0; 1291 }; 1292 1293 var dx = computeDelta(event.x, window.innerWidth); 1294 var dy = computeDelta(event.y, window.innerHeight); 1295 1296 if (dx != 0 || dy != 0) { 1297 /** @type {remoting.ClientSession} */ 1298 var that = this; 1299 /** 1300 * Scroll the view, and schedule a timer to do so again unless we've hit 1301 * the edges of the screen. This timer is cancelled when the mouse moves. 1302 * @param {number} expected The time at which we expect to be called. 1303 */ 1304 var repeatScroll = function(expected) { 1305 /** @type {number} */ 1306 var now = new Date().getTime(); 1307 /** @type {number} */ 1308 var timeout = 10; 1309 var lateAdjustment = 1 + (now - expected) / timeout; 1310 if (!that.scroll_(lateAdjustment * dx, lateAdjustment * dy)) { 1311 that.bumpScrollTimer_ = window.setTimeout( 1312 function() { repeatScroll(now + timeout); }, 1313 timeout); 1314 } 1315 }; 1316 repeatScroll(new Date().getTime()); 1317 } 1318 }; 1319 1320 /** 1321 * Sends a clipboard item to the host. 1322 * 1323 * @param {string} mimeType The MIME type of the clipboard item. 1324 * @param {string} item The clipboard item. 1325 */ 1326 remoting.ClientSession.prototype.sendClipboardItem = function(mimeType, item) { 1327 if (!this.plugin_) 1328 return; 1329 this.plugin_.sendClipboardItem(mimeType, item) 1330 }; 1331 1332