diff options
Diffstat (limited to 'remoting/webapp/client_session.js')
-rw-r--r-- | remoting/webapp/client_session.js | 461 |
1 files changed, 461 insertions, 0 deletions
diff --git a/remoting/webapp/client_session.js b/remoting/webapp/client_session.js new file mode 100644 index 0000000..da57018 --- /dev/null +++ b/remoting/webapp/client_session.js @@ -0,0 +1,461 @@ +// 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. + * + * This abstracts a <embed> element and controls the plugin which does the + * actual remoting work. There should be no UI code inside this class. It + * should be purely thought of as a controller of sorts. + */ + +'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} authenticationCode The access code for IT2Me or the + * PIN for Me2Me. + * @param {string} email The username for the talk network. + * @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, authenticationCode, + email, onStateChange) { + this.state = remoting.ClientSession.State.CREATED; + + this.hostJid = hostJid; + this.hostPublicKey = hostPublicKey; + this.authenticationCode = authenticationCode; + this.email = email; + this.clientJid = ''; + this.sessionId = ''; + /** @type {remoting.ViewerPlugin} */ + this.plugin = null; + this.logToServer = new remoting.LogToServer(); + this.onStateChange = onStateChange; + /** @type {remoting.ClientSession} */ + var that = this; + /** @type {function():void} @private */ + this.refocusPlugin_ = function() { that.plugin.focus(); }; +}; + +// 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, + CONNECTION_FAILED: 5 +}; + +/** @enum {number} */ +remoting.ClientSession.ConnectionError = { + NONE: 0, + HOST_IS_OFFLINE: 1, + SESSION_REJECTED: 2, + INCOMPATIBLE_PROTOCOL: 3, + NETWORK_FAILURE: 4 +}; + +// Keys for connection statistics. +remoting.ClientSession.STATS_KEY_VIDEO_BANDWIDTH = 'video_bandwidth'; +remoting.ClientSession.STATS_KEY_VIDEO_FRAME_RATE = 'video_frame_rate'; +remoting.ClientSession.STATS_KEY_CAPTURE_LATENCY = 'capture_latency'; +remoting.ClientSession.STATS_KEY_ENCODE_LATENCY = 'encode_latency'; +remoting.ClientSession.STATS_KEY_DECODE_LATENCY = 'decode_latency'; +remoting.ClientSession.STATS_KEY_RENDER_LATENCY = 'render_latency'; +remoting.ClientSession.STATS_KEY_ROUNDTRIP_LATENCY = 'roundtrip_latency'; + +/** + * 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 CONNECTION_FAILED. + * @type {remoting.ClientSession.ConnectionError} + */ +remoting.ClientSession.prototype.error = + remoting.ClientSession.ConnectionError.NONE; + +/** + * Chromoting session API version (for this javascript). + * This is compared with the plugin API version to verify that they are + * compatible. + * + * @const + * @private + */ +remoting.ClientSession.prototype.API_VERSION_ = 2; + +/** + * The oldest API version that we support. + * This will differ from the |API_VERSION_| if we maintain backward + * compatibility with older API versions. + * + * @const + * @private + */ +remoting.ClientSession.prototype.API_MIN_VERSION_ = 1; + +/** + * Server used to bridge into the Jabber network for establishing Jingle + * connections. + * + * @const + * @private + */ +remoting.ClientSession.prototype.HTTP_XMPP_PROXY_ = + 'https://chromoting-httpxmpp-oauth2-dev.corp.google.com'; + +/** + * 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) { }; + +/** + * 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. + * @return {void} Nothing. + */ +remoting.ClientSession.prototype.createPluginAndConnect = + function(container, oauth2AccessToken) { + this.plugin = /** @type {remoting.ViewerPlugin} */ + document.createElement('embed'); + this.plugin.id = this.PLUGIN_ID; + this.plugin.src = 'about://none'; + this.plugin.type = 'pepper-application/x-chromoting'; + this.plugin.width = 0; + this.plugin.height = 0; + this.plugin.tabIndex = 0; // Required, otherwise focus() doesn't work. + container.appendChild(this.plugin); + + this.plugin.focus(); + this.plugin.addEventListener('blur', this.refocusPlugin_, false); + + if (!this.isPluginVersionSupported_(this.plugin)) { + // TODO(ajwong): Remove from parent. + delete this.plugin; + this.setState_(remoting.ClientSession.State.BAD_PLUGIN_VERSION); + return; + } + + /** @type {remoting.ClientSession} */ var that = this; + /** @param {string} msg The IQ stanza to send. */ + this.plugin.sendIq = function(msg) { that.sendIq_(msg); }; + /** @param {string} msg The message to log. */ + this.plugin.debugInfo = function(msg) { + remoting.debug.log('plugin: ' + msg); + }; + + // TODO(ajwong): Is it even worth having this class handle these events? + // Or would it be better to just allow users to pass in their own handlers + // and leave these blank by default? + /** + * @param {number} status The plugin status. + * @param {number} error The plugin error status, if any. + */ + this.plugin.connectionInfoUpdate = function(status, error) { + that.connectionInfoUpdateCallback(status, error); + }; + this.plugin.desktopSizeUpdate = function() { that.onDesktopSizeChanged_(); }; + + // TODO(garykac): Clean exit if |connect| isn't a function. + if (typeof this.plugin.connect === 'function') { + this.connectPluginToWcs_(oauth2AccessToken); + } else { + remoting.debug.log('ERROR: remoting plugin not loaded'); + this.setState_(remoting.ClientSession.State.UNKNOWN_PLUGIN_ERROR); + } +}; + +/** + * 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.removeEventListener('blur', this.refocusPlugin_, false); + var parentNode = this.plugin.parentNode; + parentNode.removeChild(this.plugin); + this.plugin = null; + } +}; + +/** + * 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); + 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 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) { + remoting.debug.logIq(true, 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 { + remoting.debug.log('Tried to send IQ before WCS was ready.'); + this.setState_(remoting.ClientSession.State.CONNECTION_FAILED); + } +}; + +/** + * @private + * @param {remoting.ViewerPlugin} plugin The embed element for the plugin. + * @return {boolean} True if the plugin and web-app versions are compatible. + */ +remoting.ClientSession.prototype.isPluginVersionSupported_ = function(plugin) { + return this.API_VERSION_ >= plugin.apiMinVersion && + plugin.apiVersion >= this.API_MIN_VERSION_; +}; + +/** + * 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 == '') { + remoting.debug.log('Tried to connect without a full JID.'); + } + remoting.debug.setJids(this.clientJid, this.hostJid); + /** @type {remoting.ClientSession} */ + var that = this; + /** @param {string} stanza The IQ stanza received. */ + var onIq = function(stanza) { + remoting.debug.logIq(false, stanza); + if (that.plugin.onIq) { + that.plugin.onIq(stanza); + } else { + // plugin.onIq may not be set after the plugin has been shut + // down. Particularly this happens when we receive response to + // session-terminate stanza. + remoting.debug.log( + 'plugin.onIq is not set so dropping incoming message.'); + } + } + remoting.wcs.setOnIq(onIq); + that.plugin.connect(this.hostJid, this.hostPublicKey, this.clientJid, + this.authenticationCode); +}; + +/** + * Callback that the plugin invokes to indicate that the connection + * status has changed. + * + * @param {number} status The plugin's status. + * @param {number} error The plugin's error state, if any. + */ +remoting.ClientSession.prototype.connectionInfoUpdateCallback = + function(status, error) { + // Old plugins didn't pass the status and error values, so get them directly. + // Note that there is a race condition inherent in this approach. + if (typeof(status) == 'undefined') { + status = this.plugin.status; + } + if (typeof(error) == 'undefined') { + error = this.plugin.error; + } + + if (status == this.plugin.STATUS_CONNECTED) { + this.onDesktopSizeChanged_(); + } else if (status == this.plugin.STATUS_FAILED) { + this.error = /** @type {remoting.ClientSession.ConnectionError} */ (error); + } + this.setState_(/** @type {remoting.ClientSession.State} */ (status)); +}; + +/** + * @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); + } + this.logToServer.logClientSessionStateChange(this.state, this.error); +}; + +/** + * This is a callback that gets called when the window is resized. + * + * @return {void} Nothing. + */ +remoting.ClientSession.prototype.onResize = function() { + this.updateDimensions(); +}; + +/** + * 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() { + remoting.debug.log('desktop size changed: ' + + this.plugin.desktopWidth + 'x' + + this.plugin.desktopHeight); + 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 (remoting.scaleToFit) { + var scaleFitHeight = 1.0 * windowHeight / this.plugin.desktopHeight; + var scaleFitWidth = 1.0 * windowWidth / this.plugin.desktopWidth; + scale = Math.min(1.0, scaleFitHeight, scaleFitWidth); + } + + // Resize the plugin if necessary. + this.plugin.width = this.plugin.desktopWidth * scale; + this.plugin.height = this.plugin.desktopHeight * scale; + + // Position the container. + // TODO(wez): We should take into account scrollbars when positioning. + var parentNode = this.plugin.parentNode; + if (this.plugin.width < windowWidth) + parentNode.style.left = (windowWidth - this.plugin.width) / 2 + 'px'; + else + parentNode.style.left = '0'; + if (this.plugin.height < windowHeight) + parentNode.style.top = (windowHeight - this.plugin.height) / 2 + 'px'; + else + parentNode.style.top = '0'; + + remoting.debug.log('plugin dimensions: ' + + parentNode.style.left + ',' + + parentNode.style.top + '-' + + this.plugin.width + 'x' + this.plugin.height + '.'); + this.plugin.setScaleToFit(remoting.scaleToFit); +}; + +/** + * Returns an associative array with a set of stats for this connection. + * + * @return {Object.<string, number>} The connection statistics. + */ +remoting.ClientSession.prototype.stats = function() { + var dict = {}; + dict[remoting.ClientSession.STATS_KEY_VIDEO_BANDWIDTH] = + this.plugin.videoBandwidth; + dict[remoting.ClientSession.STATS_KEY_VIDEO_FRAME_RATE] = + this.plugin.videoFrameRate; + dict[remoting.ClientSession.STATS_KEY_CAPTURE_LATENCY] = + this.plugin.videoCaptureLatency; + dict[remoting.ClientSession.STATS_KEY_ENCODE_LATENCY] = + this.plugin.videoEncodeLatency; + dict[remoting.ClientSession.STATS_KEY_DECODE_LATENCY] = + this.plugin.videoDecodeLatency; + dict[remoting.ClientSession.STATS_KEY_RENDER_LATENCY] = + this.plugin.videoRenderLatency; + dict[remoting.ClientSession.STATS_KEY_ROUNDTRIP_LATENCY] = + this.plugin.roundTripLatency; + return dict; +}; + +/** + * Logs statistics. + * + * @param {Object.<string, number>} stats + */ +remoting.ClientSession.prototype.logStatistics = function(stats) { + this.logToServer.logStatistics(stats); +}; |