| // 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 || {}; |
| |
| /** |
| * @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 {string} sharedSecret The access code for IT2Me or the PIN |
| * for Me2Me. |
| * @param {string} authenticationMethods Comma-separated list of |
| * authentication methods the client should attempt to use. |
| * @param {string} authenticationTag A host-specific tag to mix into |
| * authentication hashes. |
| * @param {string} email The username for the talk network. |
| * @param {remoting.ClientSession.Mode} mode The mode of this connection. |
| * @param {function(remoting.ClientSession.State, |
| remoting.ClientSession.State):void} onStateChange |
| * The callback to invoke when the session changes state. |
| * @constructor |
| */ |
| remoting.ClientSession = function(hostJid, hostPublicKey, sharedSecret, |
| authenticationMethods, authenticationTag, |
| email, mode, onStateChange) { |
| this.state = remoting.ClientSession.State.CREATED; |
| |
| this.hostJid = hostJid; |
| this.hostPublicKey = hostPublicKey; |
| this.sharedSecret = sharedSecret; |
| this.authenticationMethods = authenticationMethods; |
| this.authenticationTag = authenticationTag; |
| this.email = email; |
| this.mode = mode; |
| this.clientJid = ''; |
| this.sessionId = ''; |
| /** @type {remoting.ClientPlugin} */ |
| this.plugin = null; |
| this.scaleToFit = false; |
| this.hasReceivedFrame_ = false; |
| this.logToServer = new remoting.LogToServer(); |
| this.onStateChange = onStateChange; |
| |
| /** @type {number?} @private */ |
| this.notifyClientDimensionsTimer_ = null; |
| |
| /** @private */ |
| this.callPluginLostFocus_ = this.pluginLostFocus_.bind(this); |
| /** @private */ |
| this.callPluginGotFocus_ = this.pluginGotFocus_.bind(this); |
| /** @private */ |
| this.callEnableShrink_ = this.setScaleToFit.bind(this, true); |
| /** @private */ |
| this.callDisableShrink_ = this.setScaleToFit.bind(this, false); |
| /** @private */ |
| this.callToggleFullScreen_ = this.toggleFullScreen_.bind(this); |
| /** @private */ |
| this.screenOptionsMenu_ = new remoting.MenuButton( |
| document.getElementById('screen-options-menu'), |
| this.onShowOptionsMenu_.bind(this)); |
| /** @private */ |
| this.sendKeysMenu_ = new remoting.MenuButton( |
| document.getElementById('send-keys-menu') |
| ); |
| |
| /** @type {HTMLElement} @private */ |
| this.shrinkToFit_ = document.getElementById('enable-shrink-to-fit'); |
| /** @type {HTMLElement} @private */ |
| this.originalSize_ = document.getElementById('disable-shrink-to-fit'); |
| /** @type {HTMLElement} @private */ |
| this.fullScreen_ = document.getElementById('toggle-full-screen'); |
| |
| this.shrinkToFit_.addEventListener('click', this.callEnableShrink_, false); |
| this.originalSize_.addEventListener('click', this.callDisableShrink_, false); |
| this.fullScreen_.addEventListener('click', this.callToggleFullScreen_, false); |
| /** @type {number?} @private */ |
| this.bumpScrollTimer_ = null; |
| /** |
| * Allow error reporting to be suppressed in situations where it would not |
| * be useful, for example, when the device is offline. |
| * |
| * @type {boolean} @private |
| */ |
| this.logErrors_ = true; |
| }; |
| |
| // 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 states transitions that occur within the web-app that have |
| // no corresponding plugin state transition. |
| /** @enum {number} */ |
| remoting.ClientSession.State = { |
| CREATED: -3, |
| BAD_PLUGIN_VERSION: -2, |
| UNKNOWN_PLUGIN_ERROR: -1, |
| UNKNOWN: 0, |
| CONNECTING: 1, |
| INITIALIZING: 2, |
| CONNECTED: 3, |
| CLOSED: 4, |
| FAILED: 5 |
| }; |
| |
| /** @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 |
| }; |
| |
| // 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'; |
| |
| /** |
| * The current state of the session. |
| * @type {remoting.ClientSession.State} |
| */ |
| remoting.ClientSession.prototype.state = remoting.ClientSession.State.UNKNOWN; |
| |
| /** |
| * The last connection error. Set when state is set to FAILED. |
| * @type {remoting.ClientSession.ConnectionError} |
| */ |
| remoting.ClientSession.prototype.error = |
| remoting.ClientSession.ConnectionError.NONE; |
| |
| /** |
| * The id of the client plugin |
| * |
| * @const |
| */ |
| remoting.ClientSession.prototype.PLUGIN_ID = 'session-client-plugin'; |
| |
| /** |
| * Callback to invoke when the state is changed. |
| * |
| * @param {remoting.ClientSession.State} oldState The previous state. |
| * @param {remoting.ClientSession.State} newState The current state. |
| */ |
| remoting.ClientSession.prototype.onStateChange = |
| function(oldState, newState) { }; |
| |
| /** |
| * @param {Element} container The element to add the plugin to. |
| * @param {string} id Id to use for the plugin element . |
| * @return {remoting.ClientPlugin} Create plugin object for the locally |
| * installed plugin. |
| */ |
| remoting.ClientSession.prototype.createClientPlugin_ = function(container, id) { |
| var plugin = /** @type {remoting.ViewerPlugin} */ |
| document.createElement('embed'); |
| |
| plugin.id = id; |
| plugin.src = 'about://none'; |
| plugin.type = 'application/vnd.chromium.remoting-viewer'; |
| plugin.width = 0; |
| plugin.height = 0; |
| plugin.tabIndex = 0; // Required, otherwise focus() doesn't work. |
| container.appendChild(plugin); |
| |
| return new remoting.ClientPluginAsync(plugin); |
| }; |
| |
| /** |
| * 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. |
| this.plugin.element().focus(); |
| } |
| } |
| }; |
| |
| /** |
| * Adds <embed> element to |container| and readies the sesion object. |
| * |
| * @param {Element} container The element to add the plugin to. |
| * @param {string} oauth2AccessToken A valid OAuth2 access token. |
| */ |
| remoting.ClientSession.prototype.createPluginAndConnect = |
| function(container, oauth2AccessToken) { |
| this.plugin = this.createClientPlugin_(container, this.PLUGIN_ID); |
| |
| this.plugin.element().focus(); |
| |
| /** @param {boolean} result */ |
| this.plugin.initialize( |
| this.onPluginInitialized_.bind(this, oauth2AccessToken)); |
| this.plugin.element().addEventListener( |
| 'focus', this.callPluginGotFocus_, false); |
| this.plugin.element().addEventListener( |
| 'blur', this.callPluginLostFocus_, false); |
| }; |
| |
| /** |
| * @param {string} oauth2AccessToken |
| * @param {boolean} initialized |
| */ |
| remoting.ClientSession.prototype.onPluginInitialized_ = |
| function(oauth2AccessToken, initialized) { |
| if (!initialized) { |
| console.error('ERROR: remoting plugin not loaded'); |
| this.plugin.cleanup(); |
| delete this.plugin; |
| this.setState_(remoting.ClientSession.State.UNKNOWN_PLUGIN_ERROR); |
| return; |
| } |
| |
| if (!this.plugin.isSupportedVersion()) { |
| this.plugin.cleanup(); |
| delete this.plugin; |
| this.setState_(remoting.ClientSession.State.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; |
| } |
| |
| // Remap the right Control key to the right Win / Cmd key on ChromeOS |
| // platforms, if the plugin has the remapKey feature. |
| if (this.plugin.hasFeature(remoting.ClientPlugin.Feature.REMAP_KEY) && |
| remoting.runningOnChromeOS()) { |
| this.plugin.remapKey(0x0700e4, 0x0700e7); |
| } |
| |
| // Enable scale-to-fit if and only if the plugin is new enough for |
| // high-quality scaling. |
| this.setScaleToFit(this.plugin.hasFeature( |
| remoting.ClientPlugin.Feature.HIGH_QUALITY_SCALING)); |
| |
| /** @param {string} msg The IQ stanza to send. */ |
| this.plugin.onOutgoingIqHandler = this.sendIq_.bind(this); |
| /** @param {string} msg The message to log. */ |
| this.plugin.onDebugMessageHandler = function(msg) { |
| console.log('plugin: ' + msg); |
| }; |
| |
| this.plugin.onConnectionStatusUpdateHandler = |
| this.onConnectionStatusUpdate_.bind(this); |
| this.plugin.onConnectionReadyHandler = |
| this.onConnectionReady_.bind(this); |
| this.plugin.onDesktopSizeUpdateHandler = |
| this.onDesktopSizeChanged_.bind(this); |
| |
| this.connectPluginToWcs_(oauth2AccessToken); |
| }; |
| |
| /** |
| * 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.cleanup(); |
| this.plugin = null; |
| } |
| this.shrinkToFit_.removeEventListener('click', this.callEnableShrink_, false); |
| this.originalSize_.removeEventListener('click', this.callDisableShrink_, |
| false); |
| this.fullScreen_.removeEventListener('click', this.callToggleFullScreen_, |
| false); |
| }; |
| |
| /** |
| * Deletes the <embed> element from the container and disconnects. |
| * |
| * @return {void} Nothing. |
| */ |
| remoting.ClientSession.prototype.disconnect = function() { |
| // The plugin won't send a state change notification, so we explicitly log |
| // the fact that the connection has closed. |
| this.logToServer.logClientSessionStateChange( |
| remoting.ClientSession.State.CLOSED, |
| remoting.ClientSession.ConnectionError.NONE, this.mode); |
| if (remoting.wcs) { |
| remoting.wcs.setOnIq(function(stanza) {}); |
| 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" ' + |
| 'initiator="' + this.clientJid + '" ' + |
| 'sid="' + this.sessionId + '">' + |
| '<reason><success/></reason>' + |
| '</jingle>' + |
| '</cli:iq>'); |
| } |
| this.removePlugin(); |
| }; |
| |
| /** |
| * 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() { |
| this.sendKeyCombination_([0x0700e0, 0x0700e2, 0x07004c]); |
| } |
| |
| /** |
| * Sends a Print Screen keypress to the remoting client. |
| * |
| * @return {void} Nothing. |
| */ |
| remoting.ClientSession.prototype.sendPrintScreen = function() { |
| this.sendKeyCombination_([0x070046]); |
| } |
| |
| /** |
| * Enables or disables the client's scale-to-fit feature. |
| * |
| * @param {boolean} scaleToFit True to enable scale-to-fit, false otherwise. |
| * @return {void} Nothing. |
| */ |
| remoting.ClientSession.prototype.setScaleToFit = function(scaleToFit) { |
| this.scaleToFit = scaleToFit; |
| this.updateDimensions(); |
| // If enabling scaling, reset bump-scroll offsets. |
| if (scaleToFit) { |
| this.scroll_(0, 0); |
| } |
| } |
| |
| /** |
| * Returns whether the client is currently scaling the host to fit the tab. |
| * |
| * @return {boolean} The current scale-to-fit setting. |
| */ |
| remoting.ClientSession.prototype.getScaleToFit = function() { |
| return this.scaleToFit; |
| } |
| |
| /** |
| * 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 an IQ stanza via the http xmpp proxy. |
| * |
| * @private |
| * @param {string} msg XML string of IQ stanza to send to server. |
| * @return {void} Nothing. |
| */ |
| remoting.ClientSession.prototype.sendIq_ = function(msg) { |
| console.log(remoting.timestamp(), remoting.formatIq.prettifySendIq(msg)); |
| // Extract the session id, so we can close the session later. |
| var parser = new DOMParser(); |
| var iqNode = parser.parseFromString(msg, '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'); |
| } |
| } |
| |
| // Send the stanza. |
| if (remoting.wcs) { |
| remoting.wcs.sendIq(msg); |
| } else { |
| console.error('Tried to send IQ before WCS was ready.'); |
| this.setState_(remoting.ClientSession.State.FAILED); |
| } |
| }; |
| |
| /** |
| * Connects the plugin to WCS. |
| * |
| * @private |
| * @param {string} oauth2AccessToken A valid OAuth2 access token. |
| * @return {void} Nothing. |
| */ |
| remoting.ClientSession.prototype.connectPluginToWcs_ = |
| function(oauth2AccessToken) { |
| this.clientJid = remoting.wcs.getJid(); |
| if (this.clientJid == '') { |
| console.error('Tried to connect without a full JID.'); |
| } |
| remoting.formatIq.setJids(this.clientJid, this.hostJid); |
| var plugin = this.plugin; |
| var forwardIq = plugin.onIncomingIq.bind(plugin); |
| /** @param {string} stanza The IQ stanza received. */ |
| var onIncomingIq = function(stanza) { |
| console.log(remoting.timestamp(), |
| remoting.formatIq.prettifyReceiveIq(stanza)); |
| forwardIq(stanza); |
| } |
| remoting.wcs.setOnIq(onIncomingIq); |
| this.plugin.connect(this.hostJid, this.hostPublicKey, this.clientJid, |
| this.sharedSecret, this.authenticationMethods, |
| this.authenticationTag); |
| }; |
| |
| /** |
| * 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.onDesktopSizeChanged_(); |
| this.plugin.notifyClientDimensions(window.innerWidth, window.innerHeight) |
| } else if (status == remoting.ClientSession.State.FAILED) { |
| this.error = /** @type {remoting.ClientSession.ConnectionError} */ (error); |
| } |
| this.setState_(/** @type {remoting.ClientSession.State} */ (status)); |
| }; |
| |
| /** |
| * 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.plugin.element().classList.add("session-client-inactive"); |
| } else { |
| this.plugin.element().classList.remove("session-client-inactive"); |
| } |
| } |
| |
| /** |
| * @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; |
| if (this.onStateChange) { |
| this.onStateChange(oldState, newState); |
| } |
| // If connection errors are being suppressed from the logs, translate |
| // FAILED to CLOSED here. This ensures that the duration is still logged. |
| var state = this.state; |
| if (this.state == remoting.ClientSession.State.FAILED && |
| !this.logErrors_) { |
| console.log('Suppressing error.'); |
| state = remoting.ClientSession.State.CLOSED; |
| } |
| this.logToServer.logClientSessionStateChange(state, this.error, this.mode); |
| }; |
| |
| /** |
| * This is a callback that gets called when the window is resized. |
| * |
| * @return {void} Nothing. |
| */ |
| remoting.ClientSession.prototype.onResize = function() { |
| this.updateDimensions(); |
| |
| if (this.notifyClientDimensionsTimer_) { |
| window.clearTimeout(this.notifyClientDimensionsTimer_); |
| this.notifyClientDimensionsTimer_ = null; |
| } |
| |
| // Defer notifying the host of the change until the window stops resizing, to |
| // avoid overloading the control channel with notifications. |
| this.notifyClientDimensionsTimer_ = window.setTimeout( |
| this.plugin.notifyClientDimensions.bind(this.plugin, |
| window.innerWidth, |
| window.innerHeight), |
| 1000); |
| |
| // If bump-scrolling is enabled, adjust the plugin margins to fully utilize |
| // the new window area. |
| this.scroll_(0, 0); |
| }; |
| |
| /** |
| * 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.desktopWidth + 'x' + |
| this.plugin.desktopHeight +' @ ' + |
| this.plugin.desktopXDpi + 'x' + |
| this.plugin.desktopYDpi + ' DPI'); |
| this.updateDimensions(); |
| }; |
| |
| /** |
| * 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.desktopWidth == 0 || |
| this.plugin.desktopHeight == 0) { |
| return; |
| } |
| |
| var windowWidth = window.innerWidth; |
| var windowHeight = window.innerHeight; |
| var scale = 1.0; |
| |
| if (this.getScaleToFit()) { |
| var scaleFitWidth = 1.0 * windowWidth / this.plugin.desktopWidth; |
| var scaleFitHeight = 1.0 * windowHeight / this.plugin.desktopHeight; |
| scale = Math.min(1.0, scaleFitHeight, scaleFitWidth); |
| } |
| |
| var width = this.plugin.desktopWidth * scale; |
| var height = this.plugin.desktopHeight * scale; |
| |
| // Resize the plugin if necessary. |
| this.plugin.element().width = width; |
| this.plugin.element().height = height; |
| |
| // Position the container. |
| // TODO(wez): We should take into account scrollbars when positioning. |
| var parentNode = this.plugin.element().parentNode; |
| |
| if (width < windowWidth) { |
| parentNode.style.left = (windowWidth - width) / 2 + 'px'; |
| } else { |
| parentNode.style.left = '0'; |
| } |
| |
| if (height < windowHeight) { |
| parentNode.style.top = (windowHeight - height) / 2 + 'px'; |
| } else { |
| parentNode.style.top = '0'; |
| } |
| |
| console.log('plugin dimensions: ' + |
| parentNode.style.left + ',' + |
| parentNode.style.top + '-' + |
| width + 'x' + height + '.'); |
| }; |
| |
| /** |
| * 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, this.mode); |
| }; |
| |
| /** |
| * Enable or disable logging of connection errors. For example, if attempting |
| * a connection using a cached JID, errors should not be logged because the |
| * JID will be refreshed and the connection retried. |
| * |
| * @param {boolean} enable True to log errors; false to suppress them. |
| */ |
| remoting.ClientSession.prototype.logErrors = function(enable) { |
| this.logErrors_ = enable; |
| }; |
| |
| /** |
| * Toggles between full-screen and windowed mode. |
| * @return {void} Nothing. |
| * @private |
| */ |
| remoting.ClientSession.prototype.toggleFullScreen_ = function() { |
| if (document.webkitIsFullScreen) { |
| document.webkitCancelFullScreen(); |
| this.enableBumpScroll_(false); |
| } else { |
| document.body.webkitRequestFullScreen(Element.ALLOW_KEYBOARD_INPUT); |
| // Don't enable bump scrolling immediately because it can result in |
| // onMouseMove firing before the webkitIsFullScreen property can be |
| // read safely (crbug.com/132180). |
| window.setTimeout(this.enableBumpScroll_.bind(this, true), 0); |
| } |
| }; |
| |
| /** |
| * Updates the options menu to reflect the current scale-to-fit and full-screen |
| * settings. |
| * @return {void} Nothing. |
| * @private |
| */ |
| remoting.ClientSession.prototype.onShowOptionsMenu_ = function() { |
| remoting.MenuButton.select(this.shrinkToFit_, this.scaleToFit); |
| remoting.MenuButton.select(this.originalSize_, !this.scaleToFit); |
| remoting.MenuButton.select(this.fullScreen_, document.webkitIsFullScreen); |
| }; |
| |
| /** |
| * 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) { |
| var plugin = this.plugin.element(); |
| var style = plugin.style; |
| |
| /** |
| * 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 stopX = { stop: false }; |
| style.marginLeft = adjustMargin(style.marginLeft, dx, |
| window.innerWidth, plugin.width, stopX); |
| var stopY = { stop: false }; |
| style.marginTop = adjustMargin(style.marginTop, dy, |
| window.innerHeight, plugin.height, stopY); |
| return stopX.stop && stopY.stop; |
| } |
| |
| /** |
| * Enable or disable bump-scrolling. |
| * @private |
| * @param {boolean} enable True to enable bump-scrolling, false to disable it. |
| */ |
| remoting.ClientSession.prototype.enableBumpScroll_ = function(enable) { |
| if (enable) { |
| /** @type {null|function(Event):void} */ |
| this.onMouseMoveRef_ = this.onMouseMove_.bind(this); |
| this.plugin.element().addEventListener('mousemove', this.onMouseMoveRef_, |
| false); |
| } else { |
| this.plugin.element().removeEventListener('mousemove', this.onMouseMoveRef_, |
| false); |
| this.onMouseMoveRef_ = null; |
| } |
| }; |
| |
| /** |
| * @param {Event} event The mouse event. |
| * @private |
| */ |
| remoting.ClientSession.prototype.onMouseMove_ = function(event) { |
| if (this.bumpScrollTimer_) { |
| window.clearTimeout(this.bumpScrollTimer_); |
| this.bumpScrollTimer_ = null; |
| } |
| // It's possible to leave content full-screen mode without using the Screen |
| // Options menu, so we disable bump scrolling as soon as we detect this. |
| if (!document.webkitIsFullScreen) { |
| this.enableBumpScroll_(false); |
| } |
| |
| /** |
| * 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 dx = computeDelta(event.x, window.innerWidth); |
| var dy = computeDelta(event.y, window.innerHeight); |
| |
| if (dx != 0 || dy != 0) { |
| /** @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.bumpScrollTimer_ = window.setTimeout( |
| function() { repeatScroll(now + timeout); }, |
| timeout); |
| } |
| }; |
| repeatScroll(new Date().getTime()); |
| } |
| }; |