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