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