client_session.js revision 5d1f7b1de12d16ceb2c938c56701a3e8bfa558f7
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} */ 23var 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 */ 48remoting.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 {HTMLMediaElement} @private */ 128 this.video_ = null; 129 130 /** @type {HTMLElement} @private */ 131 this.resizeToClientButton_ = 132 document.getElementById('screen-resize-to-client'); 133 /** @type {HTMLElement} @private */ 134 this.shrinkToFitButton_ = document.getElementById('screen-shrink-to-fit'); 135 /** @type {HTMLElement} @private */ 136 this.fullScreenButton_ = document.getElementById('toggle-full-screen'); 137 138 if (this.mode_ == remoting.ClientSession.Mode.IT2ME) { 139 // Resize-to-client is not supported for IT2Me hosts. 140 this.resizeToClientButton_.hidden = true; 141 } else { 142 this.resizeToClientButton_.hidden = false; 143 this.resizeToClientButton_.addEventListener( 144 'click', this.callSetScreenMode_, false); 145 } 146 147 this.shrinkToFitButton_.addEventListener( 148 'click', this.callSetScreenMode_, false); 149 this.fullScreenButton_.addEventListener( 150 'click', this.callToggleFullScreen_, false); 151}; 152 153/** 154 * @param {?function(remoting.ClientSession.State, 155 remoting.ClientSession.State):void} onStateChange 156 * The callback to invoke when the session changes state. 157 */ 158remoting.ClientSession.prototype.setOnStateChange = function(onStateChange) { 159 this.onStateChange_ = onStateChange; 160}; 161 162/** 163 * Called when the window or desktop size or the scaling settings change, 164 * to set the scroll-bar visibility. 165 * 166 * TODO(jamiewalch): crbug.com/252796: Remove this once crbug.com/240772 is 167 * fixed. 168 */ 169remoting.ClientSession.prototype.updateScrollbarVisibility = function() { 170 var needsVerticalScroll = false; 171 var needsHorizontalScroll = false; 172 if (!this.shrinkToFit_) { 173 // Determine whether or not horizontal or vertical scrollbars are 174 // required, taking into account their width. 175 needsVerticalScroll = window.innerHeight < this.plugin_.desktopHeight; 176 needsHorizontalScroll = window.innerWidth < this.plugin_.desktopWidth; 177 var kScrollBarWidth = 16; 178 if (needsHorizontalScroll && !needsVerticalScroll) { 179 needsVerticalScroll = 180 window.innerHeight - kScrollBarWidth < this.plugin_.desktopHeight; 181 } else if (!needsHorizontalScroll && needsVerticalScroll) { 182 needsHorizontalScroll = 183 window.innerWidth - kScrollBarWidth < this.plugin_.desktopWidth; 184 } 185 } 186 187 var htmlNode = /** @type {HTMLElement} */ (document.body.parentNode); 188 if (needsHorizontalScroll) { 189 htmlNode.classList.remove('no-horizontal-scroll'); 190 } else { 191 htmlNode.classList.add('no-horizontal-scroll'); 192 } 193 if (needsVerticalScroll) { 194 htmlNode.classList.remove('no-vertical-scroll'); 195 } else { 196 htmlNode.classList.add('no-vertical-scroll'); 197 } 198}; 199 200// Note that the positive values in both of these enums are copied directly 201// from chromoting_scriptable_object.h and must be kept in sync. The negative 202// values represent state transitions that occur within the web-app that have 203// no corresponding plugin state transition. 204/** @enum {number} */ 205remoting.ClientSession.State = { 206 CONNECTION_CANCELED: -3, // Connection closed (gracefully) before connecting. 207 CONNECTION_DROPPED: -2, // Succeeded, but subsequently closed with an error. 208 CREATED: -1, 209 UNKNOWN: 0, 210 CONNECTING: 1, 211 INITIALIZING: 2, 212 CONNECTED: 3, 213 CLOSED: 4, 214 FAILED: 5 215}; 216 217/** 218 * @param {string} state The state name. 219 * @return {remoting.ClientSession.State} The session state enum value. 220 */ 221remoting.ClientSession.State.fromString = function(state) { 222 if (!remoting.ClientSession.State.hasOwnProperty(state)) { 223 throw "Invalid ClientSession.State: " + state; 224 } 225 return remoting.ClientSession.State[state]; 226} 227 228/** @enum {number} */ 229remoting.ClientSession.ConnectionError = { 230 UNKNOWN: -1, 231 NONE: 0, 232 HOST_IS_OFFLINE: 1, 233 SESSION_REJECTED: 2, 234 INCOMPATIBLE_PROTOCOL: 3, 235 NETWORK_FAILURE: 4, 236 HOST_OVERLOAD: 5 237}; 238 239/** 240 * @param {string} error The connection error name. 241 * @return {remoting.ClientSession.ConnectionError} The connection error enum. 242 */ 243remoting.ClientSession.ConnectionError.fromString = function(error) { 244 if (!remoting.ClientSession.ConnectionError.hasOwnProperty(error)) { 245 console.error('Unexpected ClientSession.ConnectionError string: ', error); 246 return remoting.ClientSession.ConnectionError.UNKNOWN; 247 } 248 return remoting.ClientSession.ConnectionError[error]; 249} 250 251// The mode of this session. 252/** @enum {number} */ 253remoting.ClientSession.Mode = { 254 IT2ME: 0, 255 ME2ME: 1 256}; 257 258/** 259 * Type used for performance statistics collected by the plugin. 260 * @constructor 261 */ 262remoting.ClientSession.PerfStats = function() {}; 263/** @type {number} */ 264remoting.ClientSession.PerfStats.prototype.videoBandwidth; 265/** @type {number} */ 266remoting.ClientSession.PerfStats.prototype.videoFrameRate; 267/** @type {number} */ 268remoting.ClientSession.PerfStats.prototype.captureLatency; 269/** @type {number} */ 270remoting.ClientSession.PerfStats.prototype.encodeLatency; 271/** @type {number} */ 272remoting.ClientSession.PerfStats.prototype.decodeLatency; 273/** @type {number} */ 274remoting.ClientSession.PerfStats.prototype.renderLatency; 275/** @type {number} */ 276remoting.ClientSession.PerfStats.prototype.roundtripLatency; 277 278// Keys for connection statistics. 279remoting.ClientSession.STATS_KEY_VIDEO_BANDWIDTH = 'videoBandwidth'; 280remoting.ClientSession.STATS_KEY_VIDEO_FRAME_RATE = 'videoFrameRate'; 281remoting.ClientSession.STATS_KEY_CAPTURE_LATENCY = 'captureLatency'; 282remoting.ClientSession.STATS_KEY_ENCODE_LATENCY = 'encodeLatency'; 283remoting.ClientSession.STATS_KEY_DECODE_LATENCY = 'decodeLatency'; 284remoting.ClientSession.STATS_KEY_RENDER_LATENCY = 'renderLatency'; 285remoting.ClientSession.STATS_KEY_ROUNDTRIP_LATENCY = 'roundtripLatency'; 286 287// Keys for per-host settings. 288remoting.ClientSession.KEY_REMAP_KEYS = 'remapKeys'; 289remoting.ClientSession.KEY_RESIZE_TO_CLIENT = 'resizeToClient'; 290remoting.ClientSession.KEY_SHRINK_TO_FIT = 'shrinkToFit'; 291 292/** 293 * The id of the client plugin 294 * 295 * @const 296 */ 297remoting.ClientSession.prototype.PLUGIN_ID = 'session-client-plugin'; 298 299/** 300 * Set of capabilities for which hasCapability_() can be used to test. 301 * 302 * @enum {string} 303 */ 304remoting.ClientSession.Capability = { 305 // When enabled this capability causes the client to send its screen 306 // resolution to the host once connection has been established. See 307 // this.plugin_.notifyClientResolution(). 308 SEND_INITIAL_RESOLUTION: 'sendInitialResolution', 309 RATE_LIMIT_RESIZE_REQUESTS: 'rateLimitResizeRequests' 310}; 311 312/** 313 * The set of capabilities negotiated between the client and host. 314 * @type {Array.<string>} 315 * @private 316 */ 317remoting.ClientSession.prototype.capabilities_ = null; 318 319/** 320 * @param {remoting.ClientSession.Capability} capability The capability to test 321 * for. 322 * @return {boolean} True if the capability has been negotiated between 323 * the client and host. 324 * @private 325 */ 326remoting.ClientSession.prototype.hasCapability_ = function(capability) { 327 if (this.capabilities_ == null) 328 return false; 329 330 return this.capabilities_.indexOf(capability) > -1; 331}; 332 333/** 334 * @param {Element} container The element to add the plugin to. 335 * @param {string} id Id to use for the plugin element . 336 * @param {function(string, string):boolean} onExtensionMessage The handler for 337 * protocol extension messages. Returns true if a message is recognized; 338 * false otherwise. 339 * @return {remoting.ClientPlugin} Create plugin object for the locally 340 * installed plugin. 341 */ 342remoting.ClientSession.prototype.createClientPlugin_ = 343 function(container, id, onExtensionMessage) { 344 var plugin = /** @type {remoting.ViewerPlugin} */ 345 document.createElement('embed'); 346 347 plugin.id = id; 348 plugin.src = 'about://none'; 349 plugin.type = 'application/vnd.chromium.remoting-viewer'; 350 plugin.width = 0; 351 plugin.height = 0; 352 plugin.tabIndex = 0; // Required, otherwise focus() doesn't work. 353 container.appendChild(plugin); 354 355 return new remoting.ClientPlugin(plugin, onExtensionMessage); 356}; 357 358/** 359 * Callback function called when the plugin element gets focus. 360 */ 361remoting.ClientSession.prototype.pluginGotFocus_ = function() { 362 remoting.clipboard.initiateToHost(); 363}; 364 365/** 366 * Callback function called when the plugin element loses focus. 367 */ 368remoting.ClientSession.prototype.pluginLostFocus_ = function() { 369 if (this.plugin_) { 370 // Release all keys to prevent them becoming 'stuck down' on the host. 371 this.plugin_.releaseAllKeys(); 372 if (this.plugin_.element()) { 373 // Focus should stay on the element, not (for example) the toolbar. 374 this.plugin_.element().focus(); 375 } 376 } 377}; 378 379/** 380 * Adds <embed> element to |container| and readies the sesion object. 381 * 382 * @param {Element} container The element to add the plugin to. 383 * @param {function(string, string):boolean} onExtensionMessage The handler for 384 * protocol extension messages. Returns true if a message is recognized; 385 * false otherwise. 386 */ 387remoting.ClientSession.prototype.createPluginAndConnect = 388 function(container, onExtensionMessage) { 389 this.plugin_ = this.createClientPlugin_(container, this.PLUGIN_ID, 390 onExtensionMessage); 391 remoting.HostSettings.load(this.hostId_, 392 this.onHostSettingsLoaded_.bind(this)); 393}; 394 395/** 396 * @param {Object.<string>} options The current options for the host, or {} 397 * if this client has no saved settings for the host. 398 * @private 399 */ 400remoting.ClientSession.prototype.onHostSettingsLoaded_ = function(options) { 401 if (remoting.ClientSession.KEY_REMAP_KEYS in options && 402 typeof(options[remoting.ClientSession.KEY_REMAP_KEYS]) == 403 'string') { 404 this.remapKeys_ = /** @type {string} */ 405 options[remoting.ClientSession.KEY_REMAP_KEYS]; 406 } 407 if (remoting.ClientSession.KEY_RESIZE_TO_CLIENT in options && 408 typeof(options[remoting.ClientSession.KEY_RESIZE_TO_CLIENT]) == 409 'boolean') { 410 this.resizeToClient_ = /** @type {boolean} */ 411 options[remoting.ClientSession.KEY_RESIZE_TO_CLIENT]; 412 } 413 if (remoting.ClientSession.KEY_SHRINK_TO_FIT in options && 414 typeof(options[remoting.ClientSession.KEY_SHRINK_TO_FIT]) == 415 'boolean') { 416 this.shrinkToFit_ = /** @type {boolean} */ 417 options[remoting.ClientSession.KEY_SHRINK_TO_FIT]; 418 } 419 420 /** @param {boolean} result */ 421 this.plugin_.initialize(this.onPluginInitialized_.bind(this)); 422}; 423 424/** 425 * Constrains the focus to the plugin element. 426 * @private 427 */ 428remoting.ClientSession.prototype.setFocusHandlers_ = function() { 429 this.plugin_.element().addEventListener( 430 'focus', this.callPluginGotFocus_, false); 431 this.plugin_.element().addEventListener( 432 'blur', this.callPluginLostFocus_, false); 433 this.plugin_.element().focus(); 434}; 435 436/** 437 * @param {remoting.Error} error 438 */ 439remoting.ClientSession.prototype.resetWithError_ = function(error) { 440 this.plugin_.cleanup(); 441 delete this.plugin_; 442 this.error_ = error; 443 this.setState_(remoting.ClientSession.State.FAILED); 444} 445 446/** 447 * @param {boolean} initialized 448 */ 449remoting.ClientSession.prototype.onPluginInitialized_ = function(initialized) { 450 if (!initialized) { 451 console.error('ERROR: remoting plugin not loaded'); 452 this.resetWithError_(remoting.Error.MISSING_PLUGIN); 453 return; 454 } 455 456 if (!this.plugin_.isSupportedVersion()) { 457 this.resetWithError_(remoting.Error.BAD_PLUGIN_VERSION); 458 return; 459 } 460 461 // Show the Send Keys menu only if the plugin has the injectKeyEvent feature, 462 // and the Ctrl-Alt-Del button only in Me2Me mode. 463 if (!this.plugin_.hasFeature( 464 remoting.ClientPlugin.Feature.INJECT_KEY_EVENT)) { 465 var sendKeysElement = document.getElementById('send-keys-menu'); 466 sendKeysElement.hidden = true; 467 } else if (this.mode_ != remoting.ClientSession.Mode.ME2ME) { 468 var sendCadElement = document.getElementById('send-ctrl-alt-del'); 469 sendCadElement.hidden = true; 470 } 471 472 // Apply customized key remappings if the plugin supports remapKeys. 473 if (this.plugin_.hasFeature(remoting.ClientPlugin.Feature.REMAP_KEY)) { 474 this.applyRemapKeys_(true); 475 } 476 477 // Enable MediaSource-based rendering if available. 478 if (remoting.settings.USE_MEDIA_SOURCE_RENDERING && 479 this.plugin_.hasFeature( 480 remoting.ClientPlugin.Feature.MEDIA_SOURCE_RENDERING)) { 481 this.video_ = /** @type {HTMLMediaElement} */( 482 document.getElementById('mediasource-video-output')); 483 // Make sure that the <video> element is hidden until we get the first 484 // frame. 485 this.video_.style.width = '0px'; 486 this.video_.style.height = '0px'; 487 488 var renderer = new remoting.MediaSourceRenderer(this.video_); 489 this.plugin_.enableMediaSourceRendering(renderer); 490 /** @type {HTMLElement} */(document.getElementById('video-container')) 491 .classList.add('mediasource-rendering'); 492 } else { 493 /** @type {HTMLElement} */(document.getElementById('video-container')) 494 .classList.remove('mediasource-rendering'); 495 } 496 497 /** @param {string} msg The IQ stanza to send. */ 498 this.plugin_.onOutgoingIqHandler = this.sendIq_.bind(this); 499 /** @param {string} msg The message to log. */ 500 this.plugin_.onDebugMessageHandler = function(msg) { 501 console.log('plugin: ' + msg); 502 }; 503 504 this.plugin_.onConnectionStatusUpdateHandler = 505 this.onConnectionStatusUpdate_.bind(this); 506 this.plugin_.onConnectionReadyHandler = 507 this.onConnectionReady_.bind(this); 508 this.plugin_.onDesktopSizeUpdateHandler = 509 this.onDesktopSizeChanged_.bind(this); 510 this.plugin_.onSetCapabilitiesHandler = 511 this.onSetCapabilities_.bind(this); 512 this.initiateConnection_(); 513}; 514 515/** 516 * Deletes the <embed> element from the container, without sending a 517 * session_terminate request. This is to be called when the session was 518 * disconnected by the Host. 519 * 520 * @return {void} Nothing. 521 */ 522remoting.ClientSession.prototype.removePlugin = function() { 523 if (this.plugin_) { 524 this.plugin_.element().removeEventListener( 525 'focus', this.callPluginGotFocus_, false); 526 this.plugin_.element().removeEventListener( 527 'blur', this.callPluginLostFocus_, false); 528 this.plugin_.cleanup(); 529 this.plugin_ = null; 530 } 531 532 // Delete event handlers that aren't relevent when not connected. 533 this.resizeToClientButton_.removeEventListener( 534 'click', this.callSetScreenMode_, false); 535 this.shrinkToFitButton_.removeEventListener( 536 'click', this.callSetScreenMode_, false); 537 this.fullScreenButton_.removeEventListener( 538 'click', this.callToggleFullScreen_, false); 539 540 // In case the user had selected full-screen mode, cancel it now. 541 document.webkitCancelFullScreen(); 542 543 // Remove mediasource-rendering class from video-contained - this will also 544 // hide the <video> element. 545 /** @type {HTMLElement} */(document.getElementById('video-container')) 546 .classList.remove('mediasource-rendering'); 547}; 548 549/** 550 * Deletes the <embed> element from the container and disconnects. 551 * 552 * @param {boolean} isUserInitiated True for user-initiated disconnects, False 553 * for disconnects due to connection failures. 554 * @return {void} Nothing. 555 */ 556remoting.ClientSession.prototype.disconnect = function(isUserInitiated) { 557 if (isUserInitiated) { 558 // The plugin won't send a state change notification, so we explicitly log 559 // the fact that the connection has closed. 560 this.logToServer.logClientSessionStateChange( 561 remoting.ClientSession.State.CLOSED, remoting.Error.NONE, this.mode_); 562 } 563 remoting.wcsSandbox.setOnIq(null); 564 this.sendIq_( 565 '<cli:iq ' + 566 'to="' + this.hostJid_ + '" ' + 567 'type="set" ' + 568 'id="session-terminate" ' + 569 'xmlns:cli="jabber:client">' + 570 '<jingle ' + 571 'xmlns="urn:xmpp:jingle:1" ' + 572 'action="session-terminate" ' + 573 'sid="' + this.sessionId_ + '">' + 574 '<reason><success/></reason>' + 575 '</jingle>' + 576 '</cli:iq>'); 577 this.removePlugin(); 578}; 579 580/** 581 * @return {remoting.ClientSession.Mode} The current state. 582 */ 583remoting.ClientSession.prototype.getMode = function() { 584 return this.mode_; 585}; 586 587/** 588 * @return {remoting.ClientSession.State} The current state. 589 */ 590remoting.ClientSession.prototype.getState = function() { 591 return this.state_; 592}; 593 594/** 595 * @return {remoting.Error} The current error code. 596 */ 597remoting.ClientSession.prototype.getError = function() { 598 return this.error_; 599}; 600 601/** 602 * Sends a key combination to the remoting client, by sending down events for 603 * the given keys, followed by up events in reverse order. 604 * 605 * @private 606 * @param {[number]} keys Key codes to be sent. 607 * @return {void} Nothing. 608 */ 609remoting.ClientSession.prototype.sendKeyCombination_ = function(keys) { 610 for (var i = 0; i < keys.length; i++) { 611 this.plugin_.injectKeyEvent(keys[i], true); 612 } 613 for (var i = 0; i < keys.length; i++) { 614 this.plugin_.injectKeyEvent(keys[i], false); 615 } 616} 617 618/** 619 * Sends a Ctrl-Alt-Del sequence to the remoting client. 620 * 621 * @return {void} Nothing. 622 */ 623remoting.ClientSession.prototype.sendCtrlAltDel = function() { 624 this.sendKeyCombination_([0x0700e0, 0x0700e2, 0x07004c]); 625} 626 627/** 628 * Sends a Print Screen keypress to the remoting client. 629 * 630 * @return {void} Nothing. 631 */ 632remoting.ClientSession.prototype.sendPrintScreen = function() { 633 this.sendKeyCombination_([0x070046]); 634} 635 636/** 637 * Sets and stores the key remapping setting for the current host. 638 * 639 * @param {string} remappings Comma separated list of key remappings. 640 */ 641remoting.ClientSession.prototype.setRemapKeys = function(remappings) { 642 // Cancel any existing remappings and apply the new ones. 643 this.applyRemapKeys_(false); 644 this.remapKeys_ = remappings; 645 this.applyRemapKeys_(true); 646 647 // Save the new remapping setting. 648 var options = {}; 649 options[remoting.ClientSession.KEY_REMAP_KEYS] = this.remapKeys_; 650 remoting.HostSettings.save(this.hostId_, options); 651} 652 653/** 654 * Applies the configured key remappings to the session, or resets them. 655 * 656 * @param {boolean} apply True to apply remappings, false to cancel them. 657 */ 658remoting.ClientSession.prototype.applyRemapKeys_ = function(apply) { 659 // By default, under ChromeOS, remap the right Control key to the right 660 // Win / Cmd key. 661 var remapKeys = this.remapKeys_; 662 if (remapKeys == '' && remoting.runningOnChromeOS()) { 663 remapKeys = '0x0700e4>0x0700e7'; 664 } 665 666 var remappings = remapKeys.split(','); 667 for (var i = 0; i < remappings.length; ++i) { 668 var keyCodes = remappings[i].split('>'); 669 if (keyCodes.length != 2) { 670 console.log('bad remapKey: ' + remappings[i]); 671 continue; 672 } 673 var fromKey = parseInt(keyCodes[0], 0); 674 var toKey = parseInt(keyCodes[1], 0); 675 if (!fromKey || !toKey) { 676 console.log('bad remapKey code: ' + remappings[i]); 677 continue; 678 } 679 if (apply) { 680 console.log('remapKey 0x' + fromKey.toString(16) + 681 '>0x' + toKey.toString(16)); 682 this.plugin_.remapKey(fromKey, toKey); 683 } else { 684 console.log('cancel remapKey 0x' + fromKey.toString(16)); 685 this.plugin_.remapKey(fromKey, fromKey); 686 } 687 } 688} 689 690/** 691 * Callback for the two "screen mode" related menu items: Resize desktop to 692 * fit and Shrink to fit. 693 * 694 * @param {Event} event The click event indicating which mode was selected. 695 * @return {void} Nothing. 696 * @private 697 */ 698remoting.ClientSession.prototype.onSetScreenMode_ = function(event) { 699 var shrinkToFit = this.shrinkToFit_; 700 var resizeToClient = this.resizeToClient_; 701 if (event.target == this.shrinkToFitButton_) { 702 shrinkToFit = !shrinkToFit; 703 } 704 if (event.target == this.resizeToClientButton_) { 705 resizeToClient = !resizeToClient; 706 } 707 this.setScreenMode_(shrinkToFit, resizeToClient); 708}; 709 710/** 711 * Set the shrink-to-fit and resize-to-client flags and save them if this is 712 * a Me2Me connection. 713 * 714 * @param {boolean} shrinkToFit True if the remote desktop should be scaled 715 * down if it is larger than the client window; false if scroll-bars 716 * should be added in this case. 717 * @param {boolean} resizeToClient True if window resizes should cause the 718 * host to attempt to resize its desktop to match the client window size; 719 * false to disable this behaviour for subsequent window resizes--the 720 * current host desktop size is not restored in this case. 721 * @return {void} Nothing. 722 * @private 723 */ 724remoting.ClientSession.prototype.setScreenMode_ = 725 function(shrinkToFit, resizeToClient) { 726 if (resizeToClient && !this.resizeToClient_) { 727 this.plugin_.notifyClientResolution(window.innerWidth, 728 window.innerHeight, 729 window.devicePixelRatio); 730 } 731 732 // If enabling shrink, reset bump-scroll offsets. 733 var needsScrollReset = shrinkToFit && !this.shrinkToFit_; 734 735 this.shrinkToFit_ = shrinkToFit; 736 this.resizeToClient_ = resizeToClient; 737 this.updateScrollbarVisibility(); 738 739 if (this.hostId_ != '') { 740 var options = {}; 741 options[remoting.ClientSession.KEY_SHRINK_TO_FIT] = this.shrinkToFit_; 742 options[remoting.ClientSession.KEY_RESIZE_TO_CLIENT] = this.resizeToClient_; 743 remoting.HostSettings.save(this.hostId_, options); 744 } 745 746 this.updateDimensions(); 747 if (needsScrollReset) { 748 this.scroll_(0, 0); 749 } 750 751} 752 753/** 754 * Called when the client receives its first frame. 755 * 756 * @return {void} Nothing. 757 */ 758remoting.ClientSession.prototype.onFirstFrameReceived = function() { 759 this.hasReceivedFrame_ = true; 760}; 761 762/** 763 * @return {boolean} Whether the client has received a video buffer. 764 */ 765remoting.ClientSession.prototype.hasReceivedFrame = function() { 766 return this.hasReceivedFrame_; 767}; 768 769/** 770 * Sends an IQ stanza via the http xmpp proxy. 771 * 772 * @private 773 * @param {string} msg XML string of IQ stanza to send to server. 774 * @return {void} Nothing. 775 */ 776remoting.ClientSession.prototype.sendIq_ = function(msg) { 777 // Extract the session id, so we can close the session later. 778 var parser = new DOMParser(); 779 var iqNode = parser.parseFromString(msg, 'text/xml').firstChild; 780 var jingleNode = iqNode.firstChild; 781 if (jingleNode) { 782 var action = jingleNode.getAttribute('action'); 783 if (jingleNode.nodeName == 'jingle' && action == 'session-initiate') { 784 this.sessionId_ = jingleNode.getAttribute('sid'); 785 } 786 } 787 788 // HACK: Add 'x' prefix to the IDs of the outgoing messages to make sure that 789 // stanza IDs used by host and client do not match. This is necessary to 790 // workaround bug in the signaling endpoint used by chromoting. 791 // TODO(sergeyu): Remove this hack once the server-side bug is fixed. 792 var type = iqNode.getAttribute('type'); 793 if (type == 'set') { 794 var id = iqNode.getAttribute('id'); 795 iqNode.setAttribute('id', 'x' + id); 796 msg = (new XMLSerializer()).serializeToString(iqNode); 797 } 798 799 console.log(remoting.timestamp(), remoting.formatIq.prettifySendIq(msg)); 800 801 // Send the stanza. 802 remoting.wcsSandbox.sendIq(msg); 803}; 804 805remoting.ClientSession.prototype.initiateConnection_ = function() { 806 /** @type {remoting.ClientSession} */ 807 var that = this; 808 809 remoting.wcsSandbox.connect(onWcsConnected, this.resetWithError_.bind(this)); 810 811 /** @param {string} localJid Local JID. */ 812 function onWcsConnected(localJid) { 813 that.connectPluginToWcs_(localJid); 814 that.getSharedSecret_(onSharedSecretReceived.bind(null, localJid)); 815 } 816 817 /** @param {string} localJid Local JID. 818 * @param {string} sharedSecret Shared secret. */ 819 function onSharedSecretReceived(localJid, sharedSecret) { 820 that.plugin_.connect( 821 that.hostJid_, that.hostPublicKey_, localJid, sharedSecret, 822 that.authenticationMethods_, that.hostId_, that.clientPairingId_, 823 that.clientPairedSecret_); 824 }; 825} 826 827/** 828 * Connects the plugin to WCS. 829 * 830 * @private 831 * @param {string} localJid Local JID. 832 * @return {void} Nothing. 833 */ 834remoting.ClientSession.prototype.connectPluginToWcs_ = function(localJid) { 835 remoting.formatIq.setJids(localJid, this.hostJid_); 836 var forwardIq = this.plugin_.onIncomingIq.bind(this.plugin_); 837 /** @param {string} stanza The IQ stanza received. */ 838 var onIncomingIq = function(stanza) { 839 // HACK: Remove 'x' prefix added to the id in sendIq_(). 840 try { 841 var parser = new DOMParser(); 842 var iqNode = parser.parseFromString(stanza, 'text/xml').firstChild; 843 var type = iqNode.getAttribute('type'); 844 var id = iqNode.getAttribute('id'); 845 if (type != 'set' && id.charAt(0) == 'x') { 846 iqNode.setAttribute('id', id.substr(1)); 847 stanza = (new XMLSerializer()).serializeToString(iqNode); 848 } 849 } catch (err) { 850 // Pass message as is when it is malformed. 851 } 852 853 console.log(remoting.timestamp(), 854 remoting.formatIq.prettifyReceiveIq(stanza)); 855 forwardIq(stanza); 856 }; 857 remoting.wcsSandbox.setOnIq(onIncomingIq); 858} 859 860/** 861 * Gets shared secret to be used for connection. 862 * 863 * @param {function(string)} callback Callback called with the shared secret. 864 * @return {void} Nothing. 865 * @private 866 */ 867remoting.ClientSession.prototype.getSharedSecret_ = function(callback) { 868 /** @type remoting.ClientSession */ 869 var that = this; 870 if (this.plugin_.hasFeature(remoting.ClientPlugin.Feature.THIRD_PARTY_AUTH)) { 871 /** @type{function(string, string, string): void} */ 872 var fetchThirdPartyToken = function(tokenUrl, hostPublicKey, scope) { 873 that.fetchThirdPartyToken_( 874 tokenUrl, hostPublicKey, scope, 875 that.plugin_.onThirdPartyTokenFetched.bind(that.plugin_)); 876 }; 877 this.plugin_.fetchThirdPartyTokenHandler = fetchThirdPartyToken; 878 } 879 if (this.accessCode_) { 880 // Shared secret was already supplied before connecting (It2Me case). 881 callback(this.accessCode_); 882 } else if (this.plugin_.hasFeature( 883 remoting.ClientPlugin.Feature.ASYNC_PIN)) { 884 // Plugin supports asynchronously asking for the PIN. 885 this.plugin_.useAsyncPinDialog(); 886 /** @param {boolean} pairingSupported */ 887 var fetchPin = function(pairingSupported) { 888 that.fetchPin_(pairingSupported, 889 that.plugin_.onPinFetched.bind(that.plugin_)); 890 }; 891 this.plugin_.fetchPinHandler = fetchPin; 892 callback(''); 893 } else { 894 // Clients that don't support asking for a PIN asynchronously also don't 895 // support pairing, so request the PIN now without offering to remember it. 896 this.fetchPin_(false, callback); 897 } 898}; 899 900/** 901 * Callback that the plugin invokes to indicate that the connection 902 * status has changed. 903 * 904 * @private 905 * @param {number} status The plugin's status. 906 * @param {number} error The plugin's error state, if any. 907 */ 908remoting.ClientSession.prototype.onConnectionStatusUpdate_ = 909 function(status, error) { 910 if (status == remoting.ClientSession.State.CONNECTED) { 911 this.setFocusHandlers_(); 912 this.onDesktopSizeChanged_(); 913 if (this.resizeToClient_) { 914 this.plugin_.notifyClientResolution(window.innerWidth, 915 window.innerHeight, 916 window.devicePixelRatio); 917 } 918 } else if (status == remoting.ClientSession.State.FAILED) { 919 switch (error) { 920 case remoting.ClientSession.ConnectionError.HOST_IS_OFFLINE: 921 this.error_ = remoting.Error.HOST_IS_OFFLINE; 922 break; 923 case remoting.ClientSession.ConnectionError.SESSION_REJECTED: 924 this.error_ = remoting.Error.INVALID_ACCESS_CODE; 925 break; 926 case remoting.ClientSession.ConnectionError.INCOMPATIBLE_PROTOCOL: 927 this.error_ = remoting.Error.INCOMPATIBLE_PROTOCOL; 928 break; 929 case remoting.ClientSession.ConnectionError.NETWORK_FAILURE: 930 this.error_ = remoting.Error.P2P_FAILURE; 931 break; 932 case remoting.ClientSession.ConnectionError.HOST_OVERLOAD: 933 this.error_ = remoting.Error.HOST_OVERLOAD; 934 break; 935 default: 936 this.error_ = remoting.Error.UNEXPECTED; 937 } 938 } 939 this.setState_(/** @type {remoting.ClientSession.State} */ (status)); 940}; 941 942/** 943 * Callback that the plugin invokes to indicate when the connection is 944 * ready. 945 * 946 * @private 947 * @param {boolean} ready True if the connection is ready. 948 */ 949remoting.ClientSession.prototype.onConnectionReady_ = function(ready) { 950 if (!ready) { 951 this.plugin_.element().classList.add("session-client-inactive"); 952 } else { 953 this.plugin_.element().classList.remove("session-client-inactive"); 954 } 955}; 956 957/** 958 * Called when the client-host capabilities negotiation is complete. 959 * 960 * @param {!Array.<string>} capabilities The set of capabilities negotiated 961 * between the client and host. 962 * @return {void} Nothing. 963 * @private 964 */ 965remoting.ClientSession.prototype.onSetCapabilities_ = function(capabilities) { 966 if (this.capabilities_ != null) { 967 console.error('onSetCapabilities_() is called more than once'); 968 return; 969 } 970 971 this.capabilities_ = capabilities; 972 if (this.hasCapability_( 973 remoting.ClientSession.Capability.SEND_INITIAL_RESOLUTION)) { 974 this.plugin_.notifyClientResolution(window.innerWidth, 975 window.innerHeight, 976 window.devicePixelRatio); 977 } 978}; 979 980/** 981 * @private 982 * @param {remoting.ClientSession.State} newState The new state for the session. 983 * @return {void} Nothing. 984 */ 985remoting.ClientSession.prototype.setState_ = function(newState) { 986 var oldState = this.state_; 987 this.state_ = newState; 988 var state = this.state_; 989 if (oldState == remoting.ClientSession.State.CONNECTING) { 990 if (this.state_ == remoting.ClientSession.State.CLOSED) { 991 state = remoting.ClientSession.State.CONNECTION_CANCELED; 992 } else if (this.state_ == remoting.ClientSession.State.FAILED && 993 this.error_ == remoting.Error.HOST_IS_OFFLINE && 994 !this.logHostOfflineErrors_) { 995 // The application requested host-offline errors to be suppressed, for 996 // example, because this connection attempt is using a cached host JID. 997 console.log('Suppressing host-offline error.'); 998 state = remoting.ClientSession.State.CONNECTION_CANCELED; 999 } 1000 } else if (oldState == remoting.ClientSession.State.CONNECTED && 1001 this.state_ == remoting.ClientSession.State.FAILED) { 1002 state = remoting.ClientSession.State.CONNECTION_DROPPED; 1003 } 1004 this.logToServer.logClientSessionStateChange(state, this.error_, this.mode_); 1005 if (this.onStateChange_) { 1006 this.onStateChange_(oldState, newState); 1007 } 1008}; 1009 1010/** 1011 * This is a callback that gets called when the window is resized. 1012 * 1013 * @return {void} Nothing. 1014 */ 1015remoting.ClientSession.prototype.onResize = function() { 1016 this.updateDimensions(); 1017 1018 if (this.notifyClientResolutionTimer_) { 1019 window.clearTimeout(this.notifyClientResolutionTimer_); 1020 this.notifyClientResolutionTimer_ = null; 1021 } 1022 1023 // Defer notifying the host of the change until the window stops resizing, to 1024 // avoid overloading the control channel with notifications. 1025 if (this.resizeToClient_) { 1026 var kResizeRateLimitMs = 1000; 1027 if (this.hasCapability_( 1028 remoting.ClientSession.Capability.RATE_LIMIT_RESIZE_REQUESTS)) { 1029 kResizeRateLimitMs = 250; 1030 } 1031 this.notifyClientResolutionTimer_ = window.setTimeout( 1032 this.plugin_.notifyClientResolution.bind(this.plugin_, 1033 window.innerWidth, 1034 window.innerHeight, 1035 window.devicePixelRatio), 1036 kResizeRateLimitMs); 1037 } 1038 1039 // If bump-scrolling is enabled, adjust the plugin margins to fully utilize 1040 // the new window area. 1041 this.scroll_(0, 0); 1042 1043 this.updateScrollbarVisibility(); 1044}; 1045 1046/** 1047 * Requests that the host pause or resume video updates. 1048 * 1049 * @param {boolean} pause True to pause video, false to resume. 1050 * @return {void} Nothing. 1051 */ 1052remoting.ClientSession.prototype.pauseVideo = function(pause) { 1053 if (this.plugin_) { 1054 this.plugin_.pauseVideo(pause) 1055 } 1056} 1057 1058/** 1059 * Requests that the host pause or resume audio. 1060 * 1061 * @param {boolean} pause True to pause audio, false to resume. 1062 * @return {void} Nothing. 1063 */ 1064remoting.ClientSession.prototype.pauseAudio = function(pause) { 1065 if (this.plugin_) { 1066 this.plugin_.pauseAudio(pause) 1067 } 1068} 1069 1070/** 1071 * This is a callback that gets called when the plugin notifies us of a change 1072 * in the size of the remote desktop. 1073 * 1074 * @private 1075 * @return {void} Nothing. 1076 */ 1077remoting.ClientSession.prototype.onDesktopSizeChanged_ = function() { 1078 console.log('desktop size changed: ' + 1079 this.plugin_.desktopWidth + 'x' + 1080 this.plugin_.desktopHeight +' @ ' + 1081 this.plugin_.desktopXDpi + 'x' + 1082 this.plugin_.desktopYDpi + ' DPI'); 1083 this.updateDimensions(); 1084 this.updateScrollbarVisibility(); 1085}; 1086 1087/** 1088 * Refreshes the plugin's dimensions, taking into account the sizes of the 1089 * remote desktop and client window, and the current scale-to-fit setting. 1090 * 1091 * @return {void} Nothing. 1092 */ 1093remoting.ClientSession.prototype.updateDimensions = function() { 1094 if (this.plugin_.desktopWidth == 0 || 1095 this.plugin_.desktopHeight == 0) { 1096 return; 1097 } 1098 1099 var windowWidth = window.innerWidth; 1100 var windowHeight = window.innerHeight; 1101 var desktopWidth = this.plugin_.desktopWidth; 1102 var desktopHeight = this.plugin_.desktopHeight; 1103 1104 // When configured to display a host at its original size, we aim to display 1105 // it as close to its physical size as possible, without losing data: 1106 // - If client and host have matching DPI, render the host pixel-for-pixel. 1107 // - If the host has higher DPI then still render pixel-for-pixel. 1108 // - If the host has lower DPI then let Chrome up-scale it to natural size. 1109 1110 // We specify the plugin dimensions in Density-Independent Pixels, so to 1111 // render pixel-for-pixel we need to down-scale the host dimensions by the 1112 // devicePixelRatio of the client. To match the host pixel density, we choose 1113 // an initial scale factor based on the client devicePixelRatio and host DPI. 1114 1115 // Determine the effective device pixel ratio of the host, based on DPI. 1116 var hostPixelRatioX = Math.ceil(this.plugin_.desktopXDpi / 96); 1117 var hostPixelRatioY = Math.ceil(this.plugin_.desktopYDpi / 96); 1118 var hostPixelRatio = Math.min(hostPixelRatioX, hostPixelRatioY); 1119 1120 // Down-scale by the smaller of the client and host ratios. 1121 var scale = 1.0 / Math.min(window.devicePixelRatio, hostPixelRatio); 1122 1123 if (this.shrinkToFit_) { 1124 // Reduce the scale, if necessary, to fit the whole desktop in the window. 1125 var scaleFitWidth = Math.min(scale, 1.0 * windowWidth / desktopWidth); 1126 var scaleFitHeight = Math.min(scale, 1.0 * windowHeight / desktopHeight); 1127 scale = Math.min(scaleFitHeight, scaleFitWidth); 1128 1129 // If we're running full-screen then try to handle common side-by-side 1130 // multi-monitor combinations more intelligently. 1131 if (document.webkitIsFullScreen) { 1132 // If the host has two monitors each the same size as the client then 1133 // scale-to-fit will have the desktop occupy only 50% of the client area, 1134 // in which case it would be preferable to down-scale less and let the 1135 // user bump-scroll around ("scale-and-pan"). 1136 // Triggering scale-and-pan if less than 65% of the client area would be 1137 // used adds enough fuzz to cope with e.g. 1280x800 client connecting to 1138 // a (2x1280)x1024 host nicely. 1139 // Note that we don't need to account for scrollbars while fullscreen. 1140 if (scale <= scaleFitHeight * 0.65) { 1141 scale = scaleFitHeight; 1142 } 1143 if (scale <= scaleFitWidth * 0.65) { 1144 scale = scaleFitWidth; 1145 } 1146 } 1147 } 1148 1149 var pluginWidth = Math.round(desktopWidth * scale); 1150 var pluginHeight = Math.round(desktopHeight * scale); 1151 1152 if (this.video_) { 1153 this.video_.style.width = pluginWidth + 'px'; 1154 this.video_.style.height = pluginHeight + 'px'; 1155 } 1156 1157 // Resize the plugin if necessary. 1158 // TODO(wez): Handle high-DPI to high-DPI properly (crbug.com/135089). 1159 this.plugin_.element().style.width = pluginWidth + 'px'; 1160 this.plugin_.element().style.height = pluginHeight + 'px'; 1161 1162 // Position the container. 1163 // Note that clientWidth/Height take into account scrollbars. 1164 var clientWidth = document.documentElement.clientWidth; 1165 var clientHeight = document.documentElement.clientHeight; 1166 var parentNode = this.plugin_.element().parentNode; 1167 1168 console.log('plugin dimensions: ' + 1169 parentNode.style.left + ',' + 1170 parentNode.style.top + '-' + 1171 pluginWidth + 'x' + pluginHeight + '.'); 1172}; 1173 1174/** 1175 * Returns an associative array with a set of stats for this connection. 1176 * 1177 * @return {remoting.ClientSession.PerfStats} The connection statistics. 1178 */ 1179remoting.ClientSession.prototype.getPerfStats = function() { 1180 return this.plugin_.getPerfStats(); 1181}; 1182 1183/** 1184 * Logs statistics. 1185 * 1186 * @param {remoting.ClientSession.PerfStats} stats 1187 */ 1188remoting.ClientSession.prototype.logStatistics = function(stats) { 1189 this.logToServer.logStatistics(stats, this.mode_); 1190}; 1191 1192/** 1193 * Enable or disable logging of connection errors due to a host being offline. 1194 * For example, if attempting a connection using a cached JID, host-offline 1195 * errors should not be logged because the JID will be refreshed and the 1196 * connection retried. 1197 * 1198 * @param {boolean} enable True to log host-offline errors; false to suppress. 1199 */ 1200remoting.ClientSession.prototype.logHostOfflineErrors = function(enable) { 1201 this.logHostOfflineErrors_ = enable; 1202}; 1203 1204/** 1205 * Request pairing with the host for PIN-less authentication. 1206 * 1207 * @param {string} clientName The human-readable name of the client. 1208 * @param {function(string, string):void} onDone Callback to receive the 1209 * client id and shared secret when they are available. 1210 */ 1211remoting.ClientSession.prototype.requestPairing = function(clientName, onDone) { 1212 if (this.plugin_) { 1213 this.plugin_.requestPairing(clientName, onDone); 1214 } 1215}; 1216 1217/** 1218 * Toggles between full-screen and windowed mode. 1219 * @return {void} Nothing. 1220 * @private 1221 */ 1222remoting.ClientSession.prototype.toggleFullScreen_ = function() { 1223 var htmlNode = /** @type {HTMLElement} */ (document.body.parentNode); 1224 if (document.webkitIsFullScreen) { 1225 document.webkitCancelFullScreen(); 1226 this.enableBumpScroll_(false); 1227 htmlNode.classList.remove('full-screen'); 1228 } else { 1229 document.body.webkitRequestFullScreen(Element.ALLOW_KEYBOARD_INPUT); 1230 // Don't enable bump scrolling immediately because it can result in 1231 // onMouseMove firing before the webkitIsFullScreen property can be 1232 // read safely (crbug.com/132180). 1233 window.setTimeout(this.enableBumpScroll_.bind(this, true), 0); 1234 htmlNode.classList.add('full-screen'); 1235 } 1236}; 1237 1238/** 1239 * Updates the options menu to reflect the current scale-to-fit and full-screen 1240 * settings. 1241 * @return {void} Nothing. 1242 * @private 1243 */ 1244remoting.ClientSession.prototype.onShowOptionsMenu_ = function() { 1245 remoting.MenuButton.select(this.resizeToClientButton_, this.resizeToClient_); 1246 remoting.MenuButton.select(this.shrinkToFitButton_, this.shrinkToFit_); 1247 remoting.MenuButton.select(this.fullScreenButton_, 1248 document.webkitIsFullScreen); 1249}; 1250 1251/** 1252 * Scroll the client plugin by the specified amount, keeping it visible. 1253 * Note that this is only used in content full-screen mode (not windowed or 1254 * browser full-screen modes), where window.scrollBy and the scrollTop and 1255 * scrollLeft properties don't work. 1256 * @param {number} dx The amount by which to scroll horizontally. Positive to 1257 * scroll right; negative to scroll left. 1258 * @param {number} dy The amount by which to scroll vertically. Positive to 1259 * scroll down; negative to scroll up. 1260 * @return {boolean} True if the requested scroll had no effect because both 1261 * vertical and horizontal edges of the screen have been reached. 1262 * @private 1263 */ 1264remoting.ClientSession.prototype.scroll_ = function(dx, dy) { 1265 var plugin = this.plugin_.element(); 1266 var style = plugin.style; 1267 1268 /** 1269 * Helper function for x- and y-scrolling 1270 * @param {number|string} curr The current margin, eg. "10px". 1271 * @param {number} delta The requested scroll amount. 1272 * @param {number} windowBound The size of the window, in pixels. 1273 * @param {number} pluginBound The size of the plugin, in pixels. 1274 * @param {{stop: boolean}} stop Reference parameter used to indicate when 1275 * the scroll has reached one of the edges and can be stopped in that 1276 * direction. 1277 * @return {string} The new margin value. 1278 */ 1279 var adjustMargin = function(curr, delta, windowBound, pluginBound, stop) { 1280 var minMargin = Math.min(0, windowBound - pluginBound); 1281 var result = (curr ? parseFloat(curr) : 0) - delta; 1282 result = Math.min(0, Math.max(minMargin, result)); 1283 stop.stop = (result == 0 || result == minMargin); 1284 return result + "px"; 1285 }; 1286 1287 var stopX = { stop: false }; 1288 style.marginLeft = adjustMargin(style.marginLeft, dx, 1289 window.innerWidth, plugin.width, stopX); 1290 var stopY = { stop: false }; 1291 style.marginTop = adjustMargin(style.marginTop, dy, 1292 window.innerHeight, plugin.height, stopY); 1293 return stopX.stop && stopY.stop; 1294} 1295 1296/** 1297 * Enable or disable bump-scrolling. When disabling bump scrolling, also reset 1298 * the scroll offsets to (0, 0). 1299 * @private 1300 * @param {boolean} enable True to enable bump-scrolling, false to disable it. 1301 */ 1302remoting.ClientSession.prototype.enableBumpScroll_ = function(enable) { 1303 if (enable) { 1304 /** @type {null|function(Event):void} */ 1305 this.onMouseMoveRef_ = this.onMouseMove_.bind(this); 1306 this.plugin_.element().addEventListener( 1307 'mousemove', this.onMouseMoveRef_, false); 1308 } else { 1309 this.plugin_.element().removeEventListener( 1310 'mousemove', this.onMouseMoveRef_, false); 1311 this.onMouseMoveRef_ = null; 1312 this.plugin_.element().style.marginLeft = 0; 1313 this.plugin_.element().style.marginTop = 0; 1314 } 1315}; 1316 1317/** 1318 * @param {Event} event The mouse event. 1319 * @private 1320 */ 1321remoting.ClientSession.prototype.onMouseMove_ = function(event) { 1322 if (this.bumpScrollTimer_) { 1323 window.clearTimeout(this.bumpScrollTimer_); 1324 this.bumpScrollTimer_ = null; 1325 } 1326 // It's possible to leave content full-screen mode without using the Screen 1327 // Options menu, so we disable bump scrolling as soon as we detect this. 1328 if (!document.webkitIsFullScreen) { 1329 this.enableBumpScroll_(false); 1330 } 1331 1332 /** 1333 * Compute the scroll speed based on how close the mouse is to the edge. 1334 * @param {number} mousePos The mouse x- or y-coordinate 1335 * @param {number} size The width or height of the content area. 1336 * @return {number} The scroll delta, in pixels. 1337 */ 1338 var computeDelta = function(mousePos, size) { 1339 var threshold = 10; 1340 if (mousePos >= size - threshold) { 1341 return 1 + 5 * (mousePos - (size - threshold)) / threshold; 1342 } else if (mousePos <= threshold) { 1343 return -1 - 5 * (threshold - mousePos) / threshold; 1344 } 1345 return 0; 1346 }; 1347 1348 var dx = computeDelta(event.x, window.innerWidth); 1349 var dy = computeDelta(event.y, window.innerHeight); 1350 1351 if (dx != 0 || dy != 0) { 1352 /** @type {remoting.ClientSession} */ 1353 var that = this; 1354 /** 1355 * Scroll the view, and schedule a timer to do so again unless we've hit 1356 * the edges of the screen. This timer is cancelled when the mouse moves. 1357 * @param {number} expected The time at which we expect to be called. 1358 */ 1359 var repeatScroll = function(expected) { 1360 /** @type {number} */ 1361 var now = new Date().getTime(); 1362 /** @type {number} */ 1363 var timeout = 10; 1364 var lateAdjustment = 1 + (now - expected) / timeout; 1365 if (!that.scroll_(lateAdjustment * dx, lateAdjustment * dy)) { 1366 that.bumpScrollTimer_ = window.setTimeout( 1367 function() { repeatScroll(now + timeout); }, 1368 timeout); 1369 } 1370 }; 1371 repeatScroll(new Date().getTime()); 1372 } 1373}; 1374 1375/** 1376 * Sends a clipboard item to the host. 1377 * 1378 * @param {string} mimeType The MIME type of the clipboard item. 1379 * @param {string} item The clipboard item. 1380 */ 1381remoting.ClientSession.prototype.sendClipboardItem = function(mimeType, item) { 1382 if (!this.plugin_) 1383 return; 1384 this.plugin_.sendClipboardItem(mimeType, item) 1385}; 1386