| // 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, mode); |
| |
| /** @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.<string>} |
| * @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 <embed> 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.<string>} 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 <video> element is hidden until we get the first |
| // frame. |
| this.video_.style.width = '0px'; |
| this.video_.style.height = '0px'; |
| |
| var renderer = new remoting.MediaSourceRenderer(this.video_); |
| this.plugin_.enableMediaSourceRendering(renderer); |
| this.container_.classList.add('mediasource-rendering'); |
| } else { |
| this.container_.classList.remove('mediasource-rendering'); |
| } |
| |
| this.plugin_.setOnOutgoingIqHandler(this.sendIq_.bind(this)); |
| this.plugin_.setOnDebugMessageHandler(this.onDebugMessage_.bind(this)); |
| |
| this.plugin_.setConnectionStatusUpdateHandler( |
| this.onConnectionStatusUpdate_.bind(this)); |
| this.plugin_.setRouteChangedHandler(this.onRouteChanged_.bind(this)); |
| this.plugin_.setConnectionReadyHandler(this.onConnectionReady_.bind(this)); |
| this.plugin_.setDesktopSizeUpdateHandler( |
| this.onDesktopSizeChanged_.bind(this)); |
| this.plugin_.setCapabilitiesHandler(this.onSetCapabilities_.bind(this)); |
| this.plugin_.setGnubbyAuthHandler( |
| this.processGnubbyAuthMessage_.bind(this)); |
| this.plugin_.setMouseCursorHandler(this.updateMouseCursorImage_.bind(this)); |
| this.plugin_.setCastExtensionHandler( |
| this.processCastExtensionMessage_.bind(this)); |
| this.initiateConnection_(); |
| }; |
| |
| /** |
| * Deletes the <embed> element from the container, without sending a |
| * session_terminate request. This is to be called when the session was |
| * disconnected by the Host. |
| * |
| * @return {void} Nothing. |
| */ |
| remoting.ClientSession.prototype.removePlugin = function() { |
| if (this.plugin_) { |
| this.plugin_.element().removeEventListener( |
| 'focus', this.callPluginGotFocus_, false); |
| this.plugin_.element().removeEventListener( |
| 'blur', this.callPluginLostFocus_, false); |
| this.plugin_.dispose(); |
| this.plugin_ = null; |
| } |
| |
| // Leave full-screen mode, and stop listening for related events. |
| var listener = this.callOnFullScreenChanged_; |
| remoting.fullscreen.activate( |
| false, |
| function() { |
| remoting.fullscreen.removeListener(listener); |
| }); |
| if (remoting.windowFrame) { |
| remoting.windowFrame.setClientSession(null); |
| } else { |
| remoting.toolbar.setClientSession(null); |
| } |
| remoting.optionsMenu.setClientSession(null); |
| document.body.classList.remove('connected'); |
| |
| // Remove mediasource-rendering class from the container - this will also |
| // hide the <video> element. |
| this.container_.classList.remove('mediasource-rendering'); |
| |
| this.container_.removeEventListener('mousemove', |
| this.updateMouseCursorPosition_, |
| true); |
| }; |
| |
| /** |
| * Disconnect the current session with a particular |error|. The session will |
| * raise a |stateChanged| event in response to it. The caller should then call |
| * |cleanup| to remove and destroy the <embed> element. |
| * |
| * @param {remoting.Error} error The reason for the disconnection. Use |
| * remoting.Error.NONE if there is no error. |
| * @return {void} Nothing. |
| */ |
| remoting.ClientSession.prototype.disconnect = function(error) { |
| var state = (error == remoting.Error.NONE) ? |
| remoting.ClientSession.State.CLOSED : |
| remoting.ClientSession.State.FAILED; |
| |
| // The plugin won't send a state change notification, so we explicitly log |
| // the fact that the connection has closed. |
| this.logToServer.logClientSessionStateChange(state, error); |
| this.error_ = error; |
| this.setState_(state); |
| }; |
| |
| /** |
| * Deletes the <embed> element from the container and disconnects. |
| * |
| * @return {void} Nothing. |
| */ |
| remoting.ClientSession.prototype.cleanup = function() { |
| this.sendIq_( |
| '<cli:iq ' + |
| 'to="' + this.hostJid_ + '" ' + |
| 'type="set" ' + |
| 'id="session-terminate" ' + |
| 'xmlns:cli="jabber:client">' + |
| '<jingle ' + |
| 'xmlns="urn:xmpp:jingle:1" ' + |
| 'action="session-terminate" ' + |
| 'sid="' + this.sessionId_ + '">' + |
| '<reason><success/></reason>' + |
| '</jingle>' + |
| '</cli:iq>'); |
| this.removePlugin(); |
| }; |
| |
| /** |
| * @return {remoting.ClientSession.Mode} The current state. |
| */ |
| remoting.ClientSession.prototype.getMode = function() { |
| return this.mode_; |
| }; |
| |
| /** |
| * @return {remoting.ClientSession.State} The current state. |
| */ |
| remoting.ClientSession.prototype.getState = function() { |
| return this.state_; |
| }; |
| |
| /** |
| * @return {remoting.Error} The current error code. |
| */ |
| remoting.ClientSession.prototype.getError = function() { |
| return this.error_; |
| }; |
| |
| /** |
| * Sends a key combination to the remoting client, by sending down events for |
| * the given keys, followed by up events in reverse order. |
| * |
| * @private |
| * @param {[number]} keys Key codes to be sent. |
| * @return {void} Nothing. |
| */ |
| remoting.ClientSession.prototype.sendKeyCombination_ = function(keys) { |
| for (var i = 0; i < keys.length; i++) { |
| this.plugin_.injectKeyEvent(keys[i], true); |
| } |
| for (var i = 0; i < keys.length; i++) { |
| this.plugin_.injectKeyEvent(keys[i], false); |
| } |
| } |
| |
| /** |
| * Sends a Ctrl-Alt-Del sequence to the remoting client. |
| * |
| * @return {void} Nothing. |
| */ |
| remoting.ClientSession.prototype.sendCtrlAltDel = function() { |
| console.log('Sending Ctrl-Alt-Del.'); |
| this.sendKeyCombination_([0x0700e0, 0x0700e2, 0x07004c]); |
| } |
| |
| /** |
| * Sends a Print Screen keypress to the remoting client. |
| * |
| * @return {void} Nothing. |
| */ |
| remoting.ClientSession.prototype.sendPrintScreen = function() { |
| console.log('Sending Print Screen.'); |
| this.sendKeyCombination_([0x070046]); |
| } |
| |
| /** |
| * Sets and stores the key remapping setting for the current host. |
| * |
| * @param {string} remappings Comma separated list of key remappings. |
| */ |
| remoting.ClientSession.prototype.setRemapKeys = function(remappings) { |
| // Cancel any existing remappings and apply the new ones. |
| this.applyRemapKeys_(false); |
| this.remapKeys_ = remappings; |
| this.applyRemapKeys_(true); |
| |
| // Save the new remapping setting. |
| var options = {}; |
| options[remoting.ClientSession.KEY_REMAP_KEYS] = this.remapKeys_; |
| remoting.HostSettings.save(this.hostId_, options); |
| } |
| |
| /** |
| * Applies the configured key remappings to the session, or resets them. |
| * |
| * @param {boolean} apply True to apply remappings, false to cancel them. |
| */ |
| remoting.ClientSession.prototype.applyRemapKeys_ = function(apply) { |
| // By default, under ChromeOS, remap the right Control key to the right |
| // Win / Cmd key. |
| var remapKeys = this.remapKeys_; |
| if (remapKeys == '' && remoting.platformIsChromeOS()) { |
| remapKeys = '0x0700e4>0x0700e7'; |
| } |
| |
| if (remapKeys == '') { |
| return; |
| } |
| |
| var remappings = remapKeys.split(','); |
| for (var i = 0; i < remappings.length; ++i) { |
| var keyCodes = remappings[i].split('>'); |
| if (keyCodes.length != 2) { |
| console.log('bad remapKey: ' + remappings[i]); |
| continue; |
| } |
| var fromKey = parseInt(keyCodes[0], 0); |
| var toKey = parseInt(keyCodes[1], 0); |
| if (!fromKey || !toKey) { |
| console.log('bad remapKey code: ' + remappings[i]); |
| continue; |
| } |
| if (apply) { |
| console.log('remapKey 0x' + fromKey.toString(16) + |
| '>0x' + toKey.toString(16)); |
| this.plugin_.remapKey(fromKey, toKey); |
| } else { |
| console.log('cancel remapKey 0x' + fromKey.toString(16)); |
| this.plugin_.remapKey(fromKey, fromKey); |
| } |
| } |
| } |
| |
| /** |
| * Set the shrink-to-fit and resize-to-client flags and save them if this is |
| * a Me2Me connection. |
| * |
| * @param {boolean} shrinkToFit True if the remote desktop should be scaled |
| * down if it is larger than the client window; false if scroll-bars |
| * should be added in this case. |
| * @param {boolean} resizeToClient True if window resizes should cause the |
| * host to attempt to resize its desktop to match the client window size; |
| * false to disable this behaviour for subsequent window resizes--the |
| * current host desktop size is not restored in this case. |
| * @return {void} Nothing. |
| */ |
| remoting.ClientSession.prototype.setScreenMode = |
| function(shrinkToFit, resizeToClient) { |
| if (resizeToClient && !this.resizeToClient_) { |
| var clientArea = this.getClientArea_(); |
| this.plugin_.notifyClientResolution(clientArea.width, |
| clientArea.height, |
| window.devicePixelRatio); |
| } |
| |
| // If enabling shrink, reset bump-scroll offsets. |
| var needsScrollReset = shrinkToFit && !this.shrinkToFit_; |
| |
| this.shrinkToFit_ = shrinkToFit; |
| this.resizeToClient_ = resizeToClient; |
| this.updateScrollbarVisibility(); |
| |
| if (this.hostId_ != '') { |
| var options = {}; |
| options[remoting.ClientSession.KEY_SHRINK_TO_FIT] = this.shrinkToFit_; |
| options[remoting.ClientSession.KEY_RESIZE_TO_CLIENT] = this.resizeToClient_; |
| remoting.HostSettings.save(this.hostId_, options); |
| } |
| |
| this.updateDimensions(); |
| if (needsScrollReset) { |
| this.resetScroll_(); |
| } |
| |
| } |
| |
| /** |
| * Called when the client receives its first frame. |
| * |
| * @return {void} Nothing. |
| */ |
| remoting.ClientSession.prototype.onFirstFrameReceived = function() { |
| this.hasReceivedFrame_ = true; |
| }; |
| |
| /** |
| * @return {boolean} Whether the client has received a video buffer. |
| */ |
| remoting.ClientSession.prototype.hasReceivedFrame = function() { |
| return this.hasReceivedFrame_; |
| }; |
| |
| /** |
| * Sends a signaling message. |
| * |
| * @private |
| * @param {string} message XML string of IQ stanza to send to server. |
| * @return {void} Nothing. |
| */ |
| remoting.ClientSession.prototype.sendIq_ = function(message) { |
| // Extract the session id, so we can close the session later. |
| var parser = new DOMParser(); |
| var iqNode = parser.parseFromString(message, 'text/xml').firstChild; |
| var jingleNode = iqNode.firstChild; |
| if (jingleNode) { |
| var action = jingleNode.getAttribute('action'); |
| if (jingleNode.nodeName == 'jingle' && action == 'session-initiate') { |
| this.sessionId_ = jingleNode.getAttribute('sid'); |
| } |
| } |
| |
| console.log(remoting.timestamp(), remoting.formatIq.prettifySendIq(message)); |
| if (this.signalStrategy_.getState() != |
| remoting.SignalStrategy.State.CONNECTED) { |
| console.log("Message above is dropped because signaling is not connected."); |
| return; |
| } |
| |
| this.signalStrategy_.sendMessage(message); |
| }; |
| |
| /** |
| * @private |
| * @param {string} msg |
| */ |
| remoting.ClientSession.prototype.onDebugMessage_ = function(msg) { |
| console.log('plugin: ' + msg.trimRight()); |
| }; |
| |
| /** |
| * @private |
| * @param {Element} message |
| */ |
| remoting.ClientSession.prototype.onIncomingMessage_ = function(message) { |
| if (!this.plugin_) { |
| return; |
| } |
| var formatted = new XMLSerializer().serializeToString(message); |
| console.log(remoting.timestamp(), |
| remoting.formatIq.prettifyReceiveIq(formatted)); |
| this.plugin_.onIncomingIq(formatted); |
| } |
| |
| /** |
| * @private |
| */ |
| remoting.ClientSession.prototype.initiateConnection_ = function() { |
| /** @type {remoting.ClientSession} */ |
| var that = this; |
| |
| /** @param {string} sharedSecret Shared secret. */ |
| function onSharedSecretReceived(sharedSecret) { |
| that.plugin_.connect( |
| that.hostJid_, that.hostPublicKey_, that.signalStrategy_.getJid(), |
| sharedSecret, that.authenticationMethods_, that.hostId_, |
| that.clientPairingId_, that.clientPairedSecret_); |
| }; |
| |
| this.getSharedSecret_(onSharedSecretReceived); |
| } |
| |
| /** |
| * Gets shared secret to be used for connection. |
| * |
| * @param {function(string)} callback Callback called with the shared secret. |
| * @return {void} Nothing. |
| * @private |
| */ |
| remoting.ClientSession.prototype.getSharedSecret_ = function(callback) { |
| /** @type remoting.ClientSession */ |
| var that = this; |
| if (this.plugin_.hasFeature(remoting.ClientPlugin.Feature.THIRD_PARTY_AUTH)) { |
| /** @type{function(string, string, string): void} */ |
| var fetchThirdPartyToken = function(tokenUrl, hostPublicKey, scope) { |
| that.fetchThirdPartyToken_( |
| tokenUrl, hostPublicKey, scope, |
| that.plugin_.onThirdPartyTokenFetched.bind(that.plugin_)); |
| }; |
| this.plugin_.setFetchThirdPartyTokenHandler(fetchThirdPartyToken); |
| } |
| if (this.accessCode_) { |
| // Shared secret was already supplied before connecting (It2Me case). |
| callback(this.accessCode_); |
| } else if (this.plugin_.hasFeature( |
| remoting.ClientPlugin.Feature.ASYNC_PIN)) { |
| // Plugin supports asynchronously asking for the PIN. |
| this.plugin_.useAsyncPinDialog(); |
| /** @param {boolean} pairingSupported */ |
| var fetchPin = function(pairingSupported) { |
| that.fetchPin_(pairingSupported, |
| that.plugin_.onPinFetched.bind(that.plugin_)); |
| }; |
| this.plugin_.setFetchPinHandler(fetchPin); |
| callback(''); |
| } else { |
| // Clients that don't support asking for a PIN asynchronously also don't |
| // support pairing, so request the PIN now without offering to remember it. |
| this.fetchPin_(false, callback); |
| } |
| }; |
| |
| /** |
| * Callback that the plugin invokes to indicate that the connection |
| * status has changed. |
| * |
| * @private |
| * @param {number} status The plugin's status. |
| * @param {number} error The plugin's error state, if any. |
| */ |
| remoting.ClientSession.prototype.onConnectionStatusUpdate_ = |
| function(status, error) { |
| if (status == remoting.ClientSession.State.CONNECTED) { |
| this.setFocusHandlers_(); |
| this.onDesktopSizeChanged_(); |
| if (this.resizeToClient_) { |
| var clientArea = this.getClientArea_(); |
| this.plugin_.notifyClientResolution(clientArea.width, |
| clientArea.height, |
| window.devicePixelRatio); |
| } |
| // Activate full-screen related UX. |
| remoting.fullscreen.addListener(this.callOnFullScreenChanged_); |
| if (remoting.windowFrame) { |
| remoting.windowFrame.setClientSession(this); |
| } else { |
| remoting.toolbar.setClientSession(this); |
| } |
| remoting.optionsMenu.setClientSession(this); |
| document.body.classList.add('connected'); |
| |
| this.container_.addEventListener('mousemove', |
| this.updateMouseCursorPosition_, |
| true); |
| |
| } else if (status == remoting.ClientSession.State.FAILED) { |
| switch (error) { |
| case remoting.ClientSession.ConnectionError.HOST_IS_OFFLINE: |
| this.error_ = remoting.Error.HOST_IS_OFFLINE; |
| break; |
| case remoting.ClientSession.ConnectionError.SESSION_REJECTED: |
| this.error_ = remoting.Error.INVALID_ACCESS_CODE; |
| break; |
| case remoting.ClientSession.ConnectionError.INCOMPATIBLE_PROTOCOL: |
| this.error_ = remoting.Error.INCOMPATIBLE_PROTOCOL; |
| break; |
| case remoting.ClientSession.ConnectionError.NETWORK_FAILURE: |
| this.error_ = remoting.Error.P2P_FAILURE; |
| break; |
| case remoting.ClientSession.ConnectionError.HOST_OVERLOAD: |
| this.error_ = remoting.Error.HOST_OVERLOAD; |
| break; |
| default: |
| this.error_ = remoting.Error.UNEXPECTED; |
| } |
| } |
| this.setState_(/** @type {remoting.ClientSession.State} */ (status)); |
| }; |
| |
| /** |
| * Callback that the plugin invokes to indicate that the connection type for |
| * a channel has changed. |
| * |
| * @private |
| * @param {string} channel The channel name. |
| * @param {string} connectionType The new connection type. |
| */ |
| remoting.ClientSession.prototype.onRouteChanged_ = |
| function(channel, connectionType) { |
| console.log('plugin: Channel ' + channel + ' using ' + |
| connectionType + ' connection.'); |
| this.logToServer.setConnectionType(connectionType); |
| }; |
| |
| /** |
| * Callback that the plugin invokes to indicate when the connection is |
| * ready. |
| * |
| * @private |
| * @param {boolean} ready True if the connection is ready. |
| */ |
| remoting.ClientSession.prototype.onConnectionReady_ = function(ready) { |
| if (!ready) { |
| this.container_.classList.add('session-client-inactive'); |
| } else { |
| this.container_.classList.remove('session-client-inactive'); |
| } |
| |
| this.raiseEvent(remoting.ClientSession.Events.videoChannelStateChanged, |
| ready); |
| }; |
| |
| /** |
| * Called when the client-host capabilities negotiation is complete. |
| * |
| * @param {!Array.<string>} capabilities The set of capabilities negotiated |
| * between the client and host. |
| * @return {void} Nothing. |
| * @private |
| */ |
| remoting.ClientSession.prototype.onSetCapabilities_ = function(capabilities) { |
| if (this.capabilities_ != null) { |
| console.error('onSetCapabilities_() is called more than once'); |
| return; |
| } |
| |
| this.capabilities_ = capabilities; |
| if (this.hasCapability_( |
| remoting.ClientSession.Capability.SEND_INITIAL_RESOLUTION)) { |
| var clientArea = this.getClientArea_(); |
| this.plugin_.notifyClientResolution(clientArea.width, |
| clientArea.height, |
| window.devicePixelRatio); |
| } |
| if (this.hasCapability_( |
| remoting.ClientSession.Capability.VIDEO_RECORDER)) { |
| this.videoFrameRecorder_ = new remoting.VideoFrameRecorder(this.plugin_); |
| } |
| }; |
| |
| /** |
| * @private |
| * @param {remoting.ClientSession.State} newState The new state for the session. |
| * @return {void} Nothing. |
| */ |
| remoting.ClientSession.prototype.setState_ = function(newState) { |
| var oldState = this.state_; |
| this.state_ = newState; |
| var state = this.state_; |
| if (oldState == remoting.ClientSession.State.CONNECTING) { |
| if (this.state_ == remoting.ClientSession.State.CLOSED) { |
| state = remoting.ClientSession.State.CONNECTION_CANCELED; |
| } else if (this.state_ == remoting.ClientSession.State.FAILED && |
| this.error_ == remoting.Error.HOST_IS_OFFLINE && |
| !this.logHostOfflineErrors_) { |
| // The application requested host-offline errors to be suppressed, for |
| // example, because this connection attempt is using a cached host JID. |
| console.log('Suppressing host-offline error.'); |
| state = remoting.ClientSession.State.CONNECTION_CANCELED; |
| } |
| } else if (oldState == remoting.ClientSession.State.CONNECTED && |
| this.state_ == remoting.ClientSession.State.FAILED) { |
| state = remoting.ClientSession.State.CONNECTION_DROPPED; |
| } |
| this.logToServer.logClientSessionStateChange(state, this.error_); |
| if (this.state_ == remoting.ClientSession.State.CONNECTED) { |
| this.createGnubbyAuthHandler_(); |
| this.createCastExtensionHandler_(); |
| } |
| |
| this.raiseEvent(remoting.ClientSession.Events.stateChanged, |
| new remoting.ClientSession.StateEvent(newState, oldState) |
| ); |
| }; |
| |
| /** |
| * This is a callback that gets called when the window is resized. |
| * |
| * @return {void} Nothing. |
| */ |
| remoting.ClientSession.prototype.onResize = function() { |
| this.updateDimensions(); |
| |
| if (this.notifyClientResolutionTimer_) { |
| window.clearTimeout(this.notifyClientResolutionTimer_); |
| this.notifyClientResolutionTimer_ = null; |
| } |
| |
| // Defer notifying the host of the change until the window stops resizing, to |
| // avoid overloading the control channel with notifications. |
| if (this.resizeToClient_) { |
| var kResizeRateLimitMs = 1000; |
| if (this.hasCapability_( |
| remoting.ClientSession.Capability.RATE_LIMIT_RESIZE_REQUESTS)) { |
| kResizeRateLimitMs = 250; |
| } |
| var clientArea = this.getClientArea_(); |
| this.notifyClientResolutionTimer_ = window.setTimeout( |
| this.plugin_.notifyClientResolution.bind(this.plugin_, |
| clientArea.width, |
| clientArea.height, |
| window.devicePixelRatio), |
| kResizeRateLimitMs); |
| } |
| |
| // If bump-scrolling is enabled, adjust the plugin margins to fully utilize |
| // the new window area. |
| this.resetScroll_(); |
| |
| this.updateScrollbarVisibility(); |
| }; |
| |
| /** |
| * Requests that the host pause or resume video updates. |
| * |
| * @param {boolean} pause True to pause video, false to resume. |
| * @return {void} Nothing. |
| */ |
| remoting.ClientSession.prototype.pauseVideo = function(pause) { |
| if (this.plugin_) { |
| this.plugin_.pauseVideo(pause); |
| } |
| }; |
| |
| /** |
| * Requests that the host pause or resume audio. |
| * |
| * @param {boolean} pause True to pause audio, false to resume. |
| * @return {void} Nothing. |
| */ |
| remoting.ClientSession.prototype.pauseAudio = function(pause) { |
| if (this.plugin_) { |
| this.plugin_.pauseAudio(pause) |
| } |
| } |
| |
| /** |
| * This is a callback that gets called when the plugin notifies us of a change |
| * in the size of the remote desktop. |
| * |
| * @private |
| * @return {void} Nothing. |
| */ |
| remoting.ClientSession.prototype.onDesktopSizeChanged_ = function() { |
| console.log('desktop size changed: ' + |
| this.plugin_.getDesktopWidth() + 'x' + |
| this.plugin_.getDesktopHeight() +' @ ' + |
| this.plugin_.getDesktopXDpi() + 'x' + |
| this.plugin_.getDesktopYDpi() + ' DPI'); |
| this.updateDimensions(); |
| this.updateScrollbarVisibility(); |
| }; |
| |
| /** |
| * Refreshes the plugin's dimensions, taking into account the sizes of the |
| * remote desktop and client window, and the current scale-to-fit setting. |
| * |
| * @return {void} Nothing. |
| */ |
| remoting.ClientSession.prototype.updateDimensions = function() { |
| if (this.plugin_.getDesktopWidth() == 0 || |
| this.plugin_.getDesktopHeight() == 0) { |
| return; |
| } |
| |
| var clientArea = this.getClientArea_(); |
| var desktopWidth = this.plugin_.getDesktopWidth(); |
| var desktopHeight = this.plugin_.getDesktopHeight(); |
| |
| // When configured to display a host at its original size, we aim to display |
| // it as close to its physical size as possible, without losing data: |
| // - If client and host have matching DPI, render the host pixel-for-pixel. |
| // - If the host has higher DPI then still render pixel-for-pixel. |
| // - If the host has lower DPI then let Chrome up-scale it to natural size. |
| |
| // We specify the plugin dimensions in Density-Independent Pixels, so to |
| // render pixel-for-pixel we need to down-scale the host dimensions by the |
| // devicePixelRatio of the client. To match the host pixel density, we choose |
| // an initial scale factor based on the client devicePixelRatio and host DPI. |
| |
| // Determine the effective device pixel ratio of the host, based on DPI. |
| var hostPixelRatioX = Math.ceil(this.plugin_.getDesktopXDpi() / 96); |
| var hostPixelRatioY = Math.ceil(this.plugin_.getDesktopYDpi() / 96); |
| var hostPixelRatio = Math.min(hostPixelRatioX, hostPixelRatioY); |
| |
| // Down-scale by the smaller of the client and host ratios. |
| var scale = 1.0 / Math.min(window.devicePixelRatio, hostPixelRatio); |
| |
| if (this.shrinkToFit_) { |
| // Reduce the scale, if necessary, to fit the whole desktop in the window. |
| var scaleFitWidth = Math.min(scale, 1.0 * clientArea.width / desktopWidth); |
| var scaleFitHeight = |
| Math.min(scale, 1.0 * clientArea.height / desktopHeight); |
| scale = Math.min(scaleFitHeight, scaleFitWidth); |
| |
| // If we're running full-screen then try to handle common side-by-side |
| // multi-monitor combinations more intelligently. |
| if (remoting.fullscreen.isActive()) { |
| // If the host has two monitors each the same size as the client then |
| // scale-to-fit will have the desktop occupy only 50% of the client area, |
| // in which case it would be preferable to down-scale less and let the |
| // user bump-scroll around ("scale-and-pan"). |
| // Triggering scale-and-pan if less than 65% of the client area would be |
| // used adds enough fuzz to cope with e.g. 1280x800 client connecting to |
| // a (2x1280)x1024 host nicely. |
| // Note that we don't need to account for scrollbars while fullscreen. |
| if (scale <= scaleFitHeight * 0.65) { |
| scale = scaleFitHeight; |
| } |
| if (scale <= scaleFitWidth * 0.65) { |
| scale = scaleFitWidth; |
| } |
| } |
| } |
| |
| var pluginWidth = Math.round(desktopWidth * scale); |
| var pluginHeight = Math.round(desktopHeight * scale); |
| |
| if (this.video_) { |
| this.video_.style.width = pluginWidth + 'px'; |
| this.video_.style.height = pluginHeight + 'px'; |
| } |
| |
| // Resize the plugin if necessary. |
| // TODO(wez): Handle high-DPI to high-DPI properly (crbug.com/135089). |
| this.plugin_.element().style.width = pluginWidth + 'px'; |
| this.plugin_.element().style.height = pluginHeight + 'px'; |
| |
| // Position the container. |
| // Note that clientWidth/Height take into account scrollbars. |
| var clientWidth = document.documentElement.clientWidth; |
| var clientHeight = document.documentElement.clientHeight; |
| var parentNode = this.plugin_.element().parentNode; |
| |
| console.log('plugin dimensions: ' + |
| parentNode.style.left + ',' + |
| parentNode.style.top + '-' + |
| pluginWidth + 'x' + pluginHeight + '.'); |
| }; |
| |
| /** |
| * Returns an associative array with a set of stats for this connection. |
| * |
| * @return {remoting.ClientSession.PerfStats} The connection statistics. |
| */ |
| remoting.ClientSession.prototype.getPerfStats = function() { |
| return this.plugin_.getPerfStats(); |
| }; |
| |
| /** |
| * Logs statistics. |
| * |
| * @param {remoting.ClientSession.PerfStats} stats |
| */ |
| remoting.ClientSession.prototype.logStatistics = function(stats) { |
| this.logToServer.logStatistics(stats); |
| }; |
| |
| /** |
| * Enable or disable logging of connection errors due to a host being offline. |
| * For example, if attempting a connection using a cached JID, host-offline |
| * errors should not be logged because the JID will be refreshed and the |
| * connection retried. |
| * |
| * @param {boolean} enable True to log host-offline errors; false to suppress. |
| */ |
| remoting.ClientSession.prototype.logHostOfflineErrors = function(enable) { |
| this.logHostOfflineErrors_ = enable; |
| }; |
| |
| /** |
| * Request pairing with the host for PIN-less authentication. |
| * |
| * @param {string} clientName The human-readable name of the client. |
| * @param {function(string, string):void} onDone Callback to receive the |
| * client id and shared secret when they are available. |
| */ |
| remoting.ClientSession.prototype.requestPairing = function(clientName, onDone) { |
| if (this.plugin_) { |
| this.plugin_.requestPairing(clientName, onDone); |
| } |
| }; |
| |
| /** |
| * Called when the full-screen status has changed, either via the |
| * remoting.Fullscreen class, or via a system event such as the Escape key |
| * |
| * @param {boolean} fullscreen True if the app is entering full-screen mode; |
| * false if it is leaving it. |
| * @private |
| */ |
| remoting.ClientSession.prototype.onFullScreenChanged_ = function (fullscreen) { |
| var htmlNode = /** @type {HTMLElement} */ (document.documentElement); |
| this.enableBumpScroll_(fullscreen); |
| if (fullscreen) { |
| htmlNode.classList.add('full-screen'); |
| } else { |
| htmlNode.classList.remove('full-screen'); |
| } |
| }; |
| |
| /** |
| * Scroll the client plugin by the specified amount, keeping it visible. |
| * Note that this is only used in content full-screen mode (not windowed or |
| * browser full-screen modes), where window.scrollBy and the scrollTop and |
| * scrollLeft properties don't work. |
| * @param {number} dx The amount by which to scroll horizontally. Positive to |
| * scroll right; negative to scroll left. |
| * @param {number} dy The amount by which to scroll vertically. Positive to |
| * scroll down; negative to scroll up. |
| * @return {boolean} True if the requested scroll had no effect because both |
| * vertical and horizontal edges of the screen have been reached. |
| * @private |
| */ |
| remoting.ClientSession.prototype.scroll_ = function(dx, dy) { |
| /** |
| * Helper function for x- and y-scrolling |
| * @param {number|string} curr The current margin, eg. "10px". |
| * @param {number} delta The requested scroll amount. |
| * @param {number} windowBound The size of the window, in pixels. |
| * @param {number} pluginBound The size of the plugin, in pixels. |
| * @param {{stop: boolean}} stop Reference parameter used to indicate when |
| * the scroll has reached one of the edges and can be stopped in that |
| * direction. |
| * @return {string} The new margin value. |
| */ |
| var adjustMargin = function(curr, delta, windowBound, pluginBound, stop) { |
| var minMargin = Math.min(0, windowBound - pluginBound); |
| var result = (curr ? parseFloat(curr) : 0) - delta; |
| result = Math.min(0, Math.max(minMargin, result)); |
| stop.stop = (result == 0 || result == minMargin); |
| return result + 'px'; |
| }; |
| |
| var plugin = this.plugin_.element(); |
| var style = this.container_.style; |
| |
| var stopX = { stop: false }; |
| var clientArea = this.getClientArea_(); |
| style.marginLeft = adjustMargin(style.marginLeft, dx, clientArea.width, |
| this.pluginWidthForBumpScrollTesting || plugin.clientWidth, stopX); |
| |
| var stopY = { stop: false }; |
| style.marginTop = adjustMargin( |
| style.marginTop, dy, clientArea.height, |
| this.pluginHeightForBumpScrollTesting || plugin.clientHeight, stopY); |
| return stopX.stop && stopY.stop; |
| }; |
| |
| remoting.ClientSession.prototype.resetScroll_ = function() { |
| this.container_.style.marginTop = '0px'; |
| this.container_.style.marginLeft = '0px'; |
| }; |
| |
| /** |
| * Enable or disable bump-scrolling. When disabling bump scrolling, also reset |
| * the scroll offsets to (0, 0). |
| * @private |
| * @param {boolean} enable True to enable bump-scrolling, false to disable it. |
| */ |
| remoting.ClientSession.prototype.enableBumpScroll_ = function(enable) { |
| var element = /*@type{HTMLElement} */ document.documentElement; |
| if (enable) { |
| /** @type {null|function(Event):void} */ |
| this.onMouseMoveRef_ = this.onMouseMove_.bind(this); |
| element.addEventListener('mousemove', this.onMouseMoveRef_, false); |
| } else { |
| element.removeEventListener('mousemove', this.onMouseMoveRef_, false); |
| this.onMouseMoveRef_ = null; |
| this.resetScroll_(); |
| } |
| }; |
| |
| /** |
| * @param {Event} event The mouse event. |
| * @private |
| */ |
| remoting.ClientSession.prototype.onMouseMove_ = function(event) { |
| if (this.bumpScrollTimer_) { |
| window.clearTimeout(this.bumpScrollTimer_); |
| this.bumpScrollTimer_ = null; |
| } |
| |
| /** |
| * Compute the scroll speed based on how close the mouse is to the edge. |
| * @param {number} mousePos The mouse x- or y-coordinate |
| * @param {number} size The width or height of the content area. |
| * @return {number} The scroll delta, in pixels. |
| */ |
| var computeDelta = function(mousePos, size) { |
| var threshold = 10; |
| if (mousePos >= size - threshold) { |
| return 1 + 5 * (mousePos - (size - threshold)) / threshold; |
| } else if (mousePos <= threshold) { |
| return -1 - 5 * (threshold - mousePos) / threshold; |
| } |
| return 0; |
| }; |
| |
| var clientArea = this.getClientArea_(); |
| var dx = computeDelta(event.x, clientArea.width); |
| var dy = computeDelta(event.y, clientArea.height); |
| |
| if (dx != 0 || dy != 0) { |
| this.raiseEvent(remoting.ClientSession.Events.bumpScrollStarted); |
| /** @type {remoting.ClientSession} */ |
| var that = this; |
| /** |
| * Scroll the view, and schedule a timer to do so again unless we've hit |
| * the edges of the screen. This timer is cancelled when the mouse moves. |
| * @param {number} expected The time at which we expect to be called. |
| */ |
| var repeatScroll = function(expected) { |
| /** @type {number} */ |
| var now = new Date().getTime(); |
| /** @type {number} */ |
| var timeout = 10; |
| var lateAdjustment = 1 + (now - expected) / timeout; |
| if (that.scroll_(lateAdjustment * dx, lateAdjustment * dy)) { |
| that.raiseEvent(remoting.ClientSession.Events.bumpScrollStopped); |
| } else { |
| that.bumpScrollTimer_ = window.setTimeout( |
| function() { repeatScroll(now + timeout); }, |
| timeout); |
| } |
| }; |
| repeatScroll(new Date().getTime()); |
| } |
| }; |
| |
| /** |
| * Sends a clipboard item to the host. |
| * |
| * @param {string} mimeType The MIME type of the clipboard item. |
| * @param {string} item The clipboard item. |
| */ |
| remoting.ClientSession.prototype.sendClipboardItem = function(mimeType, item) { |
| if (!this.plugin_) |
| return; |
| this.plugin_.sendClipboardItem(mimeType, item); |
| }; |
| |
| /** |
| * Send a gnubby-auth extension message to the host. |
| * @param {Object} data The gnubby-auth message data. |
| */ |
| remoting.ClientSession.prototype.sendGnubbyAuthMessage = function(data) { |
| if (!this.plugin_) |
| return; |
| this.plugin_.sendClientMessage('gnubby-auth', JSON.stringify(data)); |
| }; |
| |
| /** |
| * Process a remote gnubby auth request. |
| * @param {string} data Remote gnubby request data. |
| * @private |
| */ |
| remoting.ClientSession.prototype.processGnubbyAuthMessage_ = function(data) { |
| if (this.gnubbyAuthHandler_) { |
| try { |
| this.gnubbyAuthHandler_.onMessage(data); |
| } catch (err) { |
| console.error('Failed to process gnubby message: ', |
| /** @type {*} */ (err)); |
| } |
| } else { |
| console.error('Received unexpected gnubby message'); |
| } |
| }; |
| |
| /** |
| * Create a gnubby auth handler and inform the host that gnubby auth is |
| * supported. |
| * @private |
| */ |
| remoting.ClientSession.prototype.createGnubbyAuthHandler_ = function() { |
| if (this.mode_ == remoting.ClientSession.Mode.ME2ME) { |
| this.gnubbyAuthHandler_ = new remoting.GnubbyAuthHandler(this); |
| // TODO(psj): Move to more generic capabilities mechanism. |
| this.sendGnubbyAuthMessage({'type': 'control', 'option': 'auth-v1'}); |
| } |
| }; |
| |
| /** |
| * @return {{width: number, height: number}} The height of the window's client |
| * area. This differs between apps v1 and apps v2 due to the custom window |
| * borders used by the latter. |
| * @private |
| */ |
| remoting.ClientSession.prototype.getClientArea_ = function() { |
| return remoting.windowFrame ? |
| remoting.windowFrame.getClientArea() : |
| { 'width': window.innerWidth, 'height': window.innerHeight }; |
| }; |
| |
| /** |
| * @param {string} url |
| * @param {number} hotspotX |
| * @param {number} hotspotY |
| */ |
| remoting.ClientSession.prototype.updateMouseCursorImage_ = |
| function(url, hotspotX, hotspotY) { |
| this.mouseCursorOverlay_.hidden = !url; |
| if (url) { |
| this.mouseCursorOverlay_.style.marginLeft = '-' + hotspotX + 'px'; |
| this.mouseCursorOverlay_.style.marginTop = '-' + hotspotY + 'px'; |
| this.mouseCursorOverlay_.src = url; |
| } |
| }; |
| |
| /** |
| * @return {{top: number, left:number}} The top-left corner of the plugin. |
| */ |
| remoting.ClientSession.prototype.getPluginPositionForTesting = function() { |
| var style = this.container_.style; |
| return { |
| top: parseFloat(style.marginTop), |
| left: parseFloat(style.marginLeft) |
| }; |
| }; |
| |
| /** |
| * Send a Cast extension message to the host. |
| * @param {Object} data The cast message data. |
| */ |
| remoting.ClientSession.prototype.sendCastExtensionMessage = function(data) { |
| if (!this.plugin_) |
| return; |
| this.plugin_.sendClientMessage('cast_message', JSON.stringify(data)); |
| }; |
| |
| /** |
| * Process a remote Cast extension message from the host. |
| * @param {string} data Remote cast extension data message. |
| * @private |
| */ |
| remoting.ClientSession.prototype.processCastExtensionMessage_ = function(data) { |
| if (this.castExtensionHandler_) { |
| try { |
| this.castExtensionHandler_.onMessage(data); |
| } catch (err) { |
| console.error('Failed to process cast message: ', |
| /** @type {*} */ (err)); |
| } |
| } else { |
| console.error('Received unexpected cast message'); |
| } |
| }; |
| |
| /** |
| * Create a CastExtensionHandler and inform the host that cast extension |
| * is supported. |
| * @private |
| */ |
| remoting.ClientSession.prototype.createCastExtensionHandler_ = function() { |
| if (remoting.enableCast && this.mode_ == remoting.ClientSession.Mode.ME2ME) { |
| this.castExtensionHandler_ = new remoting.CastExtensionHandler(this); |
| } |
| }; |
| |
| /** |
| * Returns true if the ClientSession can record video frames to a file. |
| * @return {boolean} |
| */ |
| remoting.ClientSession.prototype.canRecordVideo = function() { |
| return !!this.videoFrameRecorder_; |
| } |
| |
| /** |
| * Returns true if the ClientSession is currently recording video frames. |
| * @return {boolean} |
| */ |
| remoting.ClientSession.prototype.isRecordingVideo = function() { |
| if (!this.videoFrameRecorder_) { |
| return false; |
| } |
| return this.videoFrameRecorder_.isRecording(); |
| } |
| |
| /** |
| * Starts or stops recording of video frames. |
| */ |
| remoting.ClientSession.prototype.startStopRecording = function() { |
| if (this.videoFrameRecorder_) { |
| this.videoFrameRecorder_.startStopRecording(); |
| } |
| } |
| |
| /** |
| * Handles protocol extension messages. |
| * @param {string} type Type of extension message. |
| * @param {string} data Contents of the extension message. |
| * @return {boolean} True if the message was recognized, false otherwise. |
| */ |
| remoting.ClientSession.prototype.handleExtensionMessage = |
| function(type, data) { |
| if (this.videoFrameRecorder_) { |
| return this.videoFrameRecorder_.handleMessage(type, data); |
| } |
| return false; |
| } |