// Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /** * @fileoverview * Class handling creation and teardown of a remoting client session. * * The ClientSession class controls lifetime of the client plugin * object and provides the plugin with the functionality it needs to * establish connection. Specifically it: * - Delivers incoming/outgoing signaling messages, * - Adjusts plugin size and position when destop resolution changes, * * This class should not access the plugin directly, instead it should * do it through ClientPlugin class which abstracts plugin version * differences. */ 'use strict'; /** @suppress {duplicate} */ var remoting = remoting || {}; /** * True if Cast capability is supported. * * @type {boolean} */ remoting.enableCast = false; /** * @param {remoting.SignalStrategy} signalStrategy Signal strategy. * @param {HTMLElement} container Container element for the client view. * @param {string} hostDisplayName A human-readable name for the host. * @param {string} accessCode The IT2Me access code. Blank for Me2Me. * @param {function(boolean, function(string): void): void} fetchPin * Called by Me2Me connections when a PIN needs to be obtained * interactively. * @param {function(string, string, string, * function(string, string): void): void} * fetchThirdPartyToken Called by Me2Me connections when a third party * authentication token must be obtained. * @param {string} authenticationMethods Comma-separated list of * authentication methods the client should attempt to use. * @param {string} hostId The host identifier for Me2Me, or empty for IT2Me. * Mixed into authentication hashes for some authentication methods. * @param {string} hostJid The jid of the host to connect to. * @param {string} hostPublicKey The base64 encoded version of the host's * public key. * @param {remoting.ClientSession.Mode} mode The mode of this connection. * @param {string} clientPairingId For paired Me2Me connections, the * pairing id for this client, as issued by the host. * @param {string} clientPairedSecret For paired Me2Me connections, the * paired secret for this client, as issued by the host. * @constructor * @extends {base.EventSource} */ remoting.ClientSession = function(signalStrategy, container, hostDisplayName, accessCode, fetchPin, fetchThirdPartyToken, authenticationMethods, hostId, hostJid, hostPublicKey, mode, clientPairingId, clientPairedSecret) { /** @private */ this.state_ = remoting.ClientSession.State.CREATED; /** @private */ this.error_ = remoting.Error.NONE; /** @type {HTMLElement} * @private */ this.container_ = container; /** @private */ this.hostDisplayName_ = hostDisplayName; /** @private */ this.hostJid_ = hostJid; /** @private */ this.hostPublicKey_ = hostPublicKey; /** @private */ this.accessCode_ = accessCode; /** @private */ this.fetchPin_ = fetchPin; /** @private */ this.fetchThirdPartyToken_ = fetchThirdPartyToken; /** @private */ this.authenticationMethods_ = authenticationMethods; /** @private */ this.hostId_ = hostId; /** @private */ this.mode_ = mode; /** @private */ this.clientPairingId_ = clientPairingId; /** @private */ this.clientPairedSecret_ = clientPairedSecret; /** @private */ this.sessionId_ = ''; /** @type {remoting.ClientPlugin} * @private */ this.plugin_ = null; /** @private */ this.shrinkToFit_ = true; /** @private */ this.resizeToClient_ = true; /** @private */ this.remapKeys_ = ''; /** @private */ this.hasReceivedFrame_ = false; this.logToServer = new remoting.LogToServer(signalStrategy); /** @private */ this.signalStrategy_ = signalStrategy; base.debug.assert(this.signalStrategy_.getState() == remoting.SignalStrategy.State.CONNECTED); this.signalStrategy_.setIncomingStanzaCallback( this.onIncomingMessage_.bind(this)); remoting.formatIq.setJids(this.signalStrategy_.getJid(), hostJid); /** @type {number?} @private */ this.notifyClientResolutionTimer_ = null; /** @type {number?} @private */ this.bumpScrollTimer_ = null; // Bump-scroll test variables. Override to use a fake value for the width // and height of the client plugin so that bump-scrolling can be tested // without relying on the actual size of the host desktop. /** @type {number} @private */ this.pluginWidthForBumpScrollTesting = 0; /** @type {number} @private */ this.pluginHeightForBumpScrollTesting = 0; /** * Allow host-offline error reporting to be suppressed in situations where it * would not be useful, for example, when using a cached host JID. * * @type {boolean} @private */ this.logHostOfflineErrors_ = true; /** @private */ this.callPluginLostFocus_ = this.pluginLostFocus_.bind(this); /** @private */ this.callPluginGotFocus_ = this.pluginGotFocus_.bind(this); /** @private */ this.callOnFullScreenChanged_ = this.onFullScreenChanged_.bind(this) /** @type {HTMLMediaElement} @private */ this.video_ = null; /** @type {Element} @private */ this.mouseCursorOverlay_ = this.container_.querySelector('.mouse-cursor-overlay'); /** @type {Element} */ var img = this.mouseCursorOverlay_; /** @param {Event} event @private */ this.updateMouseCursorPosition_ = function(event) { img.style.top = event.y + 'px'; img.style.left = event.x + 'px'; }; /** @type {remoting.GnubbyAuthHandler} @private */ this.gnubbyAuthHandler_ = null; /** @type {remoting.CastExtensionHandler} @private */ this.castExtensionHandler_ = null; /** @type {remoting.VideoFrameRecorder} @private */ this.videoFrameRecorder_ = null; this.defineEvents(Object.keys(remoting.ClientSession.Events)); }; base.extend(remoting.ClientSession, base.EventSource); /** @enum {string} */ remoting.ClientSession.Events = { stateChanged: 'stateChanged', videoChannelStateChanged: 'videoChannelStateChanged', bumpScrollStarted: 'bumpScrollStarted', bumpScrollStopped: 'bumpScrollStopped' }; /** * Get host display name. * * @return {string} */ remoting.ClientSession.prototype.getHostDisplayName = function() { return this.hostDisplayName_; }; /** * Called when the window or desktop size or the scaling settings change, * to set the scroll-bar visibility. * * TODO(jamiewalch): crbug.com/252796: Remove this once crbug.com/240772 is * fixed. */ remoting.ClientSession.prototype.updateScrollbarVisibility = function() { var needsVerticalScroll = false; var needsHorizontalScroll = false; if (!this.shrinkToFit_) { // Determine whether or not horizontal or vertical scrollbars are // required, taking into account their width. var clientArea = this.getClientArea_(); needsVerticalScroll = clientArea.height < this.plugin_.getDesktopHeight(); needsHorizontalScroll = clientArea.width < this.plugin_.getDesktopWidth(); var kScrollBarWidth = 16; if (needsHorizontalScroll && !needsVerticalScroll) { needsVerticalScroll = clientArea.height - kScrollBarWidth < this.plugin_.getDesktopHeight(); } else if (!needsHorizontalScroll && needsVerticalScroll) { needsHorizontalScroll = clientArea.width - kScrollBarWidth < this.plugin_.getDesktopWidth(); } } var scroller = document.getElementById('scroller'); if (needsHorizontalScroll) { scroller.classList.remove('no-horizontal-scroll'); } else { scroller.classList.add('no-horizontal-scroll'); } if (needsVerticalScroll) { scroller.classList.remove('no-vertical-scroll'); } else { scroller.classList.add('no-vertical-scroll'); } }; /** * @return {boolean} True if shrink-to-fit is enabled; false otherwise. */ remoting.ClientSession.prototype.getShrinkToFit = function() { return this.shrinkToFit_; }; /** * @return {boolean} True if resize-to-client is enabled; false otherwise. */ remoting.ClientSession.prototype.getResizeToClient = function() { return this.resizeToClient_; }; // Note that the positive values in both of these enums are copied directly // from chromoting_scriptable_object.h and must be kept in sync. The negative // values represent state transitions that occur within the web-app that have // no corresponding plugin state transition. /** @enum {number} */ remoting.ClientSession.State = { CONNECTION_CANCELED: -3, // Connection closed (gracefully) before connecting. CONNECTION_DROPPED: -2, // Succeeded, but subsequently closed with an error. CREATED: -1, UNKNOWN: 0, CONNECTING: 1, INITIALIZING: 2, CONNECTED: 3, CLOSED: 4, FAILED: 5 }; /** * @param {string} state The state name. * @return {remoting.ClientSession.State} The session state enum value. */ remoting.ClientSession.State.fromString = function(state) { if (!remoting.ClientSession.State.hasOwnProperty(state)) { throw "Invalid ClientSession.State: " + state; } return remoting.ClientSession.State[state]; }; /** @constructor @param {remoting.ClientSession.State} current @param {remoting.ClientSession.State} previous */ remoting.ClientSession.StateEvent = function(current, previous) { /** @type {remoting.ClientSession.State} */ this.previous = previous /** @type {remoting.ClientSession.State} */ this.current = current; }; /** @enum {number} */ remoting.ClientSession.ConnectionError = { UNKNOWN: -1, NONE: 0, HOST_IS_OFFLINE: 1, SESSION_REJECTED: 2, INCOMPATIBLE_PROTOCOL: 3, NETWORK_FAILURE: 4, HOST_OVERLOAD: 5 }; /** * @param {string} error The connection error name. * @return {remoting.ClientSession.ConnectionError} The connection error enum. */ remoting.ClientSession.ConnectionError.fromString = function(error) { if (!remoting.ClientSession.ConnectionError.hasOwnProperty(error)) { console.error('Unexpected ClientSession.ConnectionError string: ', error); return remoting.ClientSession.ConnectionError.UNKNOWN; } return remoting.ClientSession.ConnectionError[error]; } // The mode of this session. /** @enum {number} */ remoting.ClientSession.Mode = { IT2ME: 0, ME2ME: 1 }; /** * Type used for performance statistics collected by the plugin. * @constructor */ remoting.ClientSession.PerfStats = function() {}; /** @type {number} */ remoting.ClientSession.PerfStats.prototype.videoBandwidth; /** @type {number} */ remoting.ClientSession.PerfStats.prototype.videoFrameRate; /** @type {number} */ remoting.ClientSession.PerfStats.prototype.captureLatency; /** @type {number} */ remoting.ClientSession.PerfStats.prototype.encodeLatency; /** @type {number} */ remoting.ClientSession.PerfStats.prototype.decodeLatency; /** @type {number} */ remoting.ClientSession.PerfStats.prototype.renderLatency; /** @type {number} */ remoting.ClientSession.PerfStats.prototype.roundtripLatency; // Keys for connection statistics. remoting.ClientSession.STATS_KEY_VIDEO_BANDWIDTH = 'videoBandwidth'; remoting.ClientSession.STATS_KEY_VIDEO_FRAME_RATE = 'videoFrameRate'; remoting.ClientSession.STATS_KEY_CAPTURE_LATENCY = 'captureLatency'; remoting.ClientSession.STATS_KEY_ENCODE_LATENCY = 'encodeLatency'; remoting.ClientSession.STATS_KEY_DECODE_LATENCY = 'decodeLatency'; remoting.ClientSession.STATS_KEY_RENDER_LATENCY = 'renderLatency'; remoting.ClientSession.STATS_KEY_ROUNDTRIP_LATENCY = 'roundtripLatency'; // Keys for per-host settings. remoting.ClientSession.KEY_REMAP_KEYS = 'remapKeys'; remoting.ClientSession.KEY_RESIZE_TO_CLIENT = 'resizeToClient'; remoting.ClientSession.KEY_SHRINK_TO_FIT = 'shrinkToFit'; /** * Set of capabilities for which hasCapability_() can be used to test. * * @enum {string} */ remoting.ClientSession.Capability = { // When enabled this capability causes the client to send its screen // resolution to the host once connection has been established. See // this.plugin_.notifyClientResolution(). SEND_INITIAL_RESOLUTION: 'sendInitialResolution', RATE_LIMIT_RESIZE_REQUESTS: 'rateLimitResizeRequests', VIDEO_RECORDER: 'videoRecorder', CAST: 'casting' }; /** * The set of capabilities negotiated between the client and host. * @type {Array.} * @private */ remoting.ClientSession.prototype.capabilities_ = null; /** * @param {remoting.ClientSession.Capability} capability The capability to test * for. * @return {boolean} True if the capability has been negotiated between * the client and host. * @private */ remoting.ClientSession.prototype.hasCapability_ = function(capability) { if (this.capabilities_ == null) return false; return this.capabilities_.indexOf(capability) > -1; }; /** * Callback function called when the plugin element gets focus. */ remoting.ClientSession.prototype.pluginGotFocus_ = function() { remoting.clipboard.initiateToHost(); }; /** * Callback function called when the plugin element loses focus. */ remoting.ClientSession.prototype.pluginLostFocus_ = function() { if (this.plugin_) { // Release all keys to prevent them becoming 'stuck down' on the host. this.plugin_.releaseAllKeys(); if (this.plugin_.element()) { // Focus should stay on the element, not (for example) the toolbar. // Due to crbug.com/246335, we can't restore the focus immediately, // otherwise the plugin gets confused about whether or not it has focus. window.setTimeout( this.plugin_.element().focus.bind(this.plugin_.element()), 0); } } }; /** * Adds element to |container| and readies the sesion object. * * @param {function(string, string):boolean} onExtensionMessage The handler for * protocol extension messages. Returns true if a message is recognized; * false otherwise. */ remoting.ClientSession.prototype.createPluginAndConnect = function(onExtensionMessage) { this.plugin_ = remoting.ClientPlugin.factory.createPlugin( this.container_.querySelector('.client-plugin-container'), onExtensionMessage); remoting.HostSettings.load(this.hostId_, this.onHostSettingsLoaded_.bind(this)); }; /** * @param {Object.} options The current options for the host, or {} * if this client has no saved settings for the host. * @private */ remoting.ClientSession.prototype.onHostSettingsLoaded_ = function(options) { if (remoting.ClientSession.KEY_REMAP_KEYS in options && typeof(options[remoting.ClientSession.KEY_REMAP_KEYS]) == 'string') { this.remapKeys_ = /** @type {string} */ options[remoting.ClientSession.KEY_REMAP_KEYS]; } if (remoting.ClientSession.KEY_RESIZE_TO_CLIENT in options && typeof(options[remoting.ClientSession.KEY_RESIZE_TO_CLIENT]) == 'boolean') { this.resizeToClient_ = /** @type {boolean} */ options[remoting.ClientSession.KEY_RESIZE_TO_CLIENT]; } if (remoting.ClientSession.KEY_SHRINK_TO_FIT in options && typeof(options[remoting.ClientSession.KEY_SHRINK_TO_FIT]) == 'boolean') { this.shrinkToFit_ = /** @type {boolean} */ options[remoting.ClientSession.KEY_SHRINK_TO_FIT]; } /** @param {boolean} result */ this.plugin_.initialize(this.onPluginInitialized_.bind(this)); }; /** * Constrains the focus to the plugin element. * @private */ remoting.ClientSession.prototype.setFocusHandlers_ = function() { this.plugin_.element().addEventListener( 'focus', this.callPluginGotFocus_, false); this.plugin_.element().addEventListener( 'blur', this.callPluginLostFocus_, false); this.plugin_.element().focus(); }; /** * @param {remoting.Error} error */ remoting.ClientSession.prototype.resetWithError_ = function(error) { this.signalStrategy_.setIncomingStanzaCallback(null); this.plugin_.dispose(); this.plugin_ = null; this.error_ = error; this.setState_(remoting.ClientSession.State.FAILED); } /** * @param {boolean} initialized */ remoting.ClientSession.prototype.onPluginInitialized_ = function(initialized) { if (!initialized) { console.error('ERROR: remoting plugin not loaded'); this.resetWithError_(remoting.Error.MISSING_PLUGIN); return; } if (!this.plugin_.isSupportedVersion()) { this.resetWithError_(remoting.Error.BAD_PLUGIN_VERSION); return; } // Show the Send Keys menu only if the plugin has the injectKeyEvent feature, // and the Ctrl-Alt-Del button only in Me2Me mode. if (!this.plugin_.hasFeature( remoting.ClientPlugin.Feature.INJECT_KEY_EVENT)) { var sendKeysElement = document.getElementById('send-keys-menu'); sendKeysElement.hidden = true; } else if (this.mode_ != remoting.ClientSession.Mode.ME2ME) { var sendCadElement = document.getElementById('send-ctrl-alt-del'); sendCadElement.hidden = true; } // Apply customized key remappings if the plugin supports remapKeys. if (this.plugin_.hasFeature(remoting.ClientPlugin.Feature.REMAP_KEY)) { this.applyRemapKeys_(true); } // Enable MediaSource-based rendering on Chrome 37 and above. var chromeVersionMajor = parseInt((remoting.getChromeVersion() || '0').split('.')[0], 10); if (chromeVersionMajor >= 37 && this.plugin_.hasFeature( remoting.ClientPlugin.Feature.MEDIA_SOURCE_RENDERING)) { this.video_ = /** @type {HTMLMediaElement} */( this.container_.querySelector('video')); // Make sure that the