| OLD | NEW |
| (Empty) |
| 1 // Copyright (c) 2012 The Chromium Authors. All rights reserved. | |
| 2 // Use of this source code is governed by a BSD-style license that can be | |
| 3 // found in the LICENSE file. | |
| 4 | |
| 5 /** | |
| 6 * @fileoverview | |
| 7 * Class handling creation and teardown of a remoting client session. | |
| 8 * | |
| 9 * This abstracts a <embed> element and controls the plugin which does the | |
| 10 * actual remoting work. There should be no UI code inside this class. It | |
| 11 * should be purely thought of as a controller of sorts. | |
| 12 */ | |
| 13 | |
| 14 'use strict'; | |
| 15 | |
| 16 /** @suppress {duplicate} */ | |
| 17 var remoting = remoting || {}; | |
| 18 | |
| 19 /** | |
| 20 * @param {string} hostJid The jid of the host to connect to. | |
| 21 * @param {string} hostPublicKey The base64 encoded version of the host's | |
| 22 * public key. | |
| 23 * @param {string} authenticationCode The access code for IT2Me or the | |
| 24 * PIN for Me2Me. | |
| 25 * @param {string} email The username for the talk network. | |
| 26 * @param {function(remoting.ClientSession.State, | |
| 27 remoting.ClientSession.State):void} onStateChange | |
| 28 * The callback to invoke when the session changes state. | |
| 29 * @constructor | |
| 30 */ | |
| 31 remoting.ClientSession = function(hostJid, hostPublicKey, authenticationCode, | |
| 32 email, onStateChange) { | |
| 33 this.state = remoting.ClientSession.State.CREATED; | |
| 34 | |
| 35 this.hostJid = hostJid; | |
| 36 this.hostPublicKey = hostPublicKey; | |
| 37 this.authenticationCode = authenticationCode; | |
| 38 this.email = email; | |
| 39 this.clientJid = ''; | |
| 40 this.sessionId = ''; | |
| 41 /** @type {remoting.ViewerPlugin} */ | |
| 42 this.plugin = null; | |
| 43 this.logToServer = new remoting.LogToServer(); | |
| 44 this.onStateChange = onStateChange; | |
| 45 /** @type {remoting.ClientSession} */ | |
| 46 var that = this; | |
| 47 /** @type {function():void} @private */ | |
| 48 this.refocusPlugin_ = function() { that.plugin.focus(); }; | |
| 49 }; | |
| 50 | |
| 51 // Note that the positive values in both of these enums are copied directly | |
| 52 // from chromoting_scriptable_object.h and must be kept in sync. The negative | |
| 53 // values represent states transitions that occur within the web-app that have | |
| 54 // no corresponding plugin state transition. | |
| 55 /** @enum {number} */ | |
| 56 remoting.ClientSession.State = { | |
| 57 CREATED: -3, | |
| 58 BAD_PLUGIN_VERSION: -2, | |
| 59 UNKNOWN_PLUGIN_ERROR: -1, | |
| 60 UNKNOWN: 0, | |
| 61 CONNECTING: 1, | |
| 62 INITIALIZING: 2, | |
| 63 CONNECTED: 3, | |
| 64 CLOSED: 4, | |
| 65 CONNECTION_FAILED: 5 | |
| 66 }; | |
| 67 | |
| 68 /** @enum {number} */ | |
| 69 remoting.ClientSession.ConnectionError = { | |
| 70 NONE: 0, | |
| 71 HOST_IS_OFFLINE: 1, | |
| 72 SESSION_REJECTED: 2, | |
| 73 INCOMPATIBLE_PROTOCOL: 3, | |
| 74 NETWORK_FAILURE: 4 | |
| 75 }; | |
| 76 | |
| 77 // Keys for connection statistics. | |
| 78 remoting.ClientSession.STATS_KEY_VIDEO_BANDWIDTH = 'video_bandwidth'; | |
| 79 remoting.ClientSession.STATS_KEY_VIDEO_FRAME_RATE = 'video_frame_rate'; | |
| 80 remoting.ClientSession.STATS_KEY_CAPTURE_LATENCY = 'capture_latency'; | |
| 81 remoting.ClientSession.STATS_KEY_ENCODE_LATENCY = 'encode_latency'; | |
| 82 remoting.ClientSession.STATS_KEY_DECODE_LATENCY = 'decode_latency'; | |
| 83 remoting.ClientSession.STATS_KEY_RENDER_LATENCY = 'render_latency'; | |
| 84 remoting.ClientSession.STATS_KEY_ROUNDTRIP_LATENCY = 'roundtrip_latency'; | |
| 85 | |
| 86 /** | |
| 87 * The current state of the session. | |
| 88 * @type {remoting.ClientSession.State} | |
| 89 */ | |
| 90 remoting.ClientSession.prototype.state = remoting.ClientSession.State.UNKNOWN; | |
| 91 | |
| 92 /** | |
| 93 * The last connection error. Set when state is set to CONNECTION_FAILED. | |
| 94 * @type {remoting.ClientSession.ConnectionError} | |
| 95 */ | |
| 96 remoting.ClientSession.prototype.error = | |
| 97 remoting.ClientSession.ConnectionError.NONE; | |
| 98 | |
| 99 /** | |
| 100 * Chromoting session API version (for this javascript). | |
| 101 * This is compared with the plugin API version to verify that they are | |
| 102 * compatible. | |
| 103 * | |
| 104 * @const | |
| 105 * @private | |
| 106 */ | |
| 107 remoting.ClientSession.prototype.API_VERSION_ = 2; | |
| 108 | |
| 109 /** | |
| 110 * The oldest API version that we support. | |
| 111 * This will differ from the |API_VERSION_| if we maintain backward | |
| 112 * compatibility with older API versions. | |
| 113 * | |
| 114 * @const | |
| 115 * @private | |
| 116 */ | |
| 117 remoting.ClientSession.prototype.API_MIN_VERSION_ = 1; | |
| 118 | |
| 119 /** | |
| 120 * Server used to bridge into the Jabber network for establishing Jingle | |
| 121 * connections. | |
| 122 * | |
| 123 * @const | |
| 124 * @private | |
| 125 */ | |
| 126 remoting.ClientSession.prototype.HTTP_XMPP_PROXY_ = | |
| 127 'https://chromoting-httpxmpp-oauth2-dev.corp.google.com'; | |
| 128 | |
| 129 /** | |
| 130 * The id of the client plugin | |
| 131 * | |
| 132 * @const | |
| 133 */ | |
| 134 remoting.ClientSession.prototype.PLUGIN_ID = 'session-client-plugin'; | |
| 135 | |
| 136 /** | |
| 137 * Callback to invoke when the state is changed. | |
| 138 * | |
| 139 * @param {remoting.ClientSession.State} oldState The previous state. | |
| 140 * @param {remoting.ClientSession.State} newState The current state. | |
| 141 */ | |
| 142 remoting.ClientSession.prototype.onStateChange = | |
| 143 function(oldState, newState) { }; | |
| 144 | |
| 145 /** | |
| 146 * Adds <embed> element to |container| and readies the sesion object. | |
| 147 * | |
| 148 * @param {Element} container The element to add the plugin to. | |
| 149 * @param {string} oauth2AccessToken A valid OAuth2 access token. | |
| 150 * @return {void} Nothing. | |
| 151 */ | |
| 152 remoting.ClientSession.prototype.createPluginAndConnect = | |
| 153 function(container, oauth2AccessToken) { | |
| 154 this.plugin = /** @type {remoting.ViewerPlugin} */ | |
| 155 document.createElement('embed'); | |
| 156 this.plugin.id = this.PLUGIN_ID; | |
| 157 this.plugin.src = 'about://none'; | |
| 158 this.plugin.type = 'pepper-application/x-chromoting'; | |
| 159 this.plugin.width = 0; | |
| 160 this.plugin.height = 0; | |
| 161 this.plugin.tabIndex = 0; // Required, otherwise focus() doesn't work. | |
| 162 container.appendChild(this.plugin); | |
| 163 | |
| 164 this.plugin.focus(); | |
| 165 this.plugin.addEventListener('blur', this.refocusPlugin_, false); | |
| 166 | |
| 167 if (!this.isPluginVersionSupported_(this.plugin)) { | |
| 168 // TODO(ajwong): Remove from parent. | |
| 169 delete this.plugin; | |
| 170 this.setState_(remoting.ClientSession.State.BAD_PLUGIN_VERSION); | |
| 171 return; | |
| 172 } | |
| 173 | |
| 174 /** @type {remoting.ClientSession} */ var that = this; | |
| 175 /** @param {string} msg The IQ stanza to send. */ | |
| 176 this.plugin.sendIq = function(msg) { that.sendIq_(msg); }; | |
| 177 /** @param {string} msg The message to log. */ | |
| 178 this.plugin.debugInfo = function(msg) { | |
| 179 remoting.debug.log('plugin: ' + msg); | |
| 180 }; | |
| 181 | |
| 182 // TODO(ajwong): Is it even worth having this class handle these events? | |
| 183 // Or would it be better to just allow users to pass in their own handlers | |
| 184 // and leave these blank by default? | |
| 185 /** | |
| 186 * @param {number} status The plugin status. | |
| 187 * @param {number} error The plugin error status, if any. | |
| 188 */ | |
| 189 this.plugin.connectionInfoUpdate = function(status, error) { | |
| 190 that.connectionInfoUpdateCallback(status, error); | |
| 191 }; | |
| 192 this.plugin.desktopSizeUpdate = function() { that.onDesktopSizeChanged_(); }; | |
| 193 | |
| 194 // TODO(garykac): Clean exit if |connect| isn't a function. | |
| 195 if (typeof this.plugin.connect === 'function') { | |
| 196 this.connectPluginToWcs_(oauth2AccessToken); | |
| 197 } else { | |
| 198 remoting.debug.log('ERROR: remoting plugin not loaded'); | |
| 199 this.setState_(remoting.ClientSession.State.UNKNOWN_PLUGIN_ERROR); | |
| 200 } | |
| 201 }; | |
| 202 | |
| 203 /** | |
| 204 * Deletes the <embed> element from the container, without sending a | |
| 205 * session_terminate request. This is to be called when the session was | |
| 206 * disconnected by the Host. | |
| 207 * | |
| 208 * @return {void} Nothing. | |
| 209 */ | |
| 210 remoting.ClientSession.prototype.removePlugin = function() { | |
| 211 if (this.plugin) { | |
| 212 this.plugin.removeEventListener('blur', this.refocusPlugin_, false); | |
| 213 var parentNode = this.plugin.parentNode; | |
| 214 parentNode.removeChild(this.plugin); | |
| 215 this.plugin = null; | |
| 216 } | |
| 217 }; | |
| 218 | |
| 219 /** | |
| 220 * Deletes the <embed> element from the container and disconnects. | |
| 221 * | |
| 222 * @return {void} Nothing. | |
| 223 */ | |
| 224 remoting.ClientSession.prototype.disconnect = function() { | |
| 225 // The plugin won't send a state change notification, so we explicitly log | |
| 226 // the fact that the connection has closed. | |
| 227 this.logToServer.logClientSessionStateChange( | |
| 228 remoting.ClientSession.State.CLOSED, | |
| 229 remoting.ClientSession.ConnectionError.NONE); | |
| 230 if (remoting.wcs) { | |
| 231 remoting.wcs.setOnIq(function(stanza) {}); | |
| 232 this.sendIq_( | |
| 233 '<cli:iq ' + | |
| 234 'to="' + this.hostJid + '" ' + | |
| 235 'type="set" ' + | |
| 236 'id="session-terminate" ' + | |
| 237 'xmlns:cli="jabber:client">' + | |
| 238 '<jingle ' + | |
| 239 'xmlns="urn:xmpp:jingle:1" ' + | |
| 240 'action="session-terminate" ' + | |
| 241 'initiator="' + this.clientJid + '" ' + | |
| 242 'sid="' + this.sessionId + '">' + | |
| 243 '<reason><success/></reason>' + | |
| 244 '</jingle>' + | |
| 245 '</cli:iq>'); | |
| 246 } | |
| 247 this.removePlugin(); | |
| 248 }; | |
| 249 | |
| 250 /** | |
| 251 * Sends an IQ stanza via the http xmpp proxy. | |
| 252 * | |
| 253 * @private | |
| 254 * @param {string} msg XML string of IQ stanza to send to server. | |
| 255 * @return {void} Nothing. | |
| 256 */ | |
| 257 remoting.ClientSession.prototype.sendIq_ = function(msg) { | |
| 258 remoting.debug.logIq(true, msg); | |
| 259 // Extract the session id, so we can close the session later. | |
| 260 var parser = new DOMParser(); | |
| 261 var iqNode = parser.parseFromString(msg, 'text/xml').firstChild; | |
| 262 var jingleNode = iqNode.firstChild; | |
| 263 if (jingleNode) { | |
| 264 var action = jingleNode.getAttribute('action'); | |
| 265 if (jingleNode.nodeName == 'jingle' && action == 'session-initiate') { | |
| 266 this.sessionId = jingleNode.getAttribute('sid'); | |
| 267 } | |
| 268 } | |
| 269 | |
| 270 // Send the stanza. | |
| 271 if (remoting.wcs) { | |
| 272 remoting.wcs.sendIq(msg); | |
| 273 } else { | |
| 274 remoting.debug.log('Tried to send IQ before WCS was ready.'); | |
| 275 this.setState_(remoting.ClientSession.State.CONNECTION_FAILED); | |
| 276 } | |
| 277 }; | |
| 278 | |
| 279 /** | |
| 280 * @private | |
| 281 * @param {remoting.ViewerPlugin} plugin The embed element for the plugin. | |
| 282 * @return {boolean} True if the plugin and web-app versions are compatible. | |
| 283 */ | |
| 284 remoting.ClientSession.prototype.isPluginVersionSupported_ = function(plugin) { | |
| 285 return this.API_VERSION_ >= plugin.apiMinVersion && | |
| 286 plugin.apiVersion >= this.API_MIN_VERSION_; | |
| 287 }; | |
| 288 | |
| 289 /** | |
| 290 * Connects the plugin to WCS. | |
| 291 * | |
| 292 * @private | |
| 293 * @param {string} oauth2AccessToken A valid OAuth2 access token. | |
| 294 * @return {void} Nothing. | |
| 295 */ | |
| 296 remoting.ClientSession.prototype.connectPluginToWcs_ = | |
| 297 function(oauth2AccessToken) { | |
| 298 this.clientJid = remoting.wcs.getJid(); | |
| 299 if (this.clientJid == '') { | |
| 300 remoting.debug.log('Tried to connect without a full JID.'); | |
| 301 } | |
| 302 remoting.debug.setJids(this.clientJid, this.hostJid); | |
| 303 /** @type {remoting.ClientSession} */ | |
| 304 var that = this; | |
| 305 /** @param {string} stanza The IQ stanza received. */ | |
| 306 var onIq = function(stanza) { | |
| 307 remoting.debug.logIq(false, stanza); | |
| 308 if (that.plugin.onIq) { | |
| 309 that.plugin.onIq(stanza); | |
| 310 } else { | |
| 311 // plugin.onIq may not be set after the plugin has been shut | |
| 312 // down. Particularly this happens when we receive response to | |
| 313 // session-terminate stanza. | |
| 314 remoting.debug.log( | |
| 315 'plugin.onIq is not set so dropping incoming message.'); | |
| 316 } | |
| 317 } | |
| 318 remoting.wcs.setOnIq(onIq); | |
| 319 that.plugin.connect(this.hostJid, this.hostPublicKey, this.clientJid, | |
| 320 this.authenticationCode); | |
| 321 }; | |
| 322 | |
| 323 /** | |
| 324 * Callback that the plugin invokes to indicate that the connection | |
| 325 * status has changed. | |
| 326 * | |
| 327 * @param {number} status The plugin's status. | |
| 328 * @param {number} error The plugin's error state, if any. | |
| 329 */ | |
| 330 remoting.ClientSession.prototype.connectionInfoUpdateCallback = | |
| 331 function(status, error) { | |
| 332 // Old plugins didn't pass the status and error values, so get them directly. | |
| 333 // Note that there is a race condition inherent in this approach. | |
| 334 if (typeof(status) == 'undefined') { | |
| 335 status = this.plugin.status; | |
| 336 } | |
| 337 if (typeof(error) == 'undefined') { | |
| 338 error = this.plugin.error; | |
| 339 } | |
| 340 | |
| 341 if (status == this.plugin.STATUS_CONNECTED) { | |
| 342 this.onDesktopSizeChanged_(); | |
| 343 } else if (status == this.plugin.STATUS_FAILED) { | |
| 344 this.error = /** @type {remoting.ClientSession.ConnectionError} */ (error); | |
| 345 } | |
| 346 this.setState_(/** @type {remoting.ClientSession.State} */ (status)); | |
| 347 }; | |
| 348 | |
| 349 /** | |
| 350 * @private | |
| 351 * @param {remoting.ClientSession.State} newState The new state for the session. | |
| 352 * @return {void} Nothing. | |
| 353 */ | |
| 354 remoting.ClientSession.prototype.setState_ = function(newState) { | |
| 355 var oldState = this.state; | |
| 356 this.state = newState; | |
| 357 if (this.onStateChange) { | |
| 358 this.onStateChange(oldState, newState); | |
| 359 } | |
| 360 this.logToServer.logClientSessionStateChange(this.state, this.error); | |
| 361 }; | |
| 362 | |
| 363 /** | |
| 364 * This is a callback that gets called when the window is resized. | |
| 365 * | |
| 366 * @return {void} Nothing. | |
| 367 */ | |
| 368 remoting.ClientSession.prototype.onResize = function() { | |
| 369 this.updateDimensions(); | |
| 370 }; | |
| 371 | |
| 372 /** | |
| 373 * This is a callback that gets called when the plugin notifies us of a change | |
| 374 * in the size of the remote desktop. | |
| 375 * | |
| 376 * @private | |
| 377 * @return {void} Nothing. | |
| 378 */ | |
| 379 remoting.ClientSession.prototype.onDesktopSizeChanged_ = function() { | |
| 380 remoting.debug.log('desktop size changed: ' + | |
| 381 this.plugin.desktopWidth + 'x' + | |
| 382 this.plugin.desktopHeight); | |
| 383 this.updateDimensions(); | |
| 384 }; | |
| 385 | |
| 386 /** | |
| 387 * Refreshes the plugin's dimensions, taking into account the sizes of the | |
| 388 * remote desktop and client window, and the current scale-to-fit setting. | |
| 389 * | |
| 390 * @return {void} Nothing. | |
| 391 */ | |
| 392 remoting.ClientSession.prototype.updateDimensions = function() { | |
| 393 if (this.plugin.desktopWidth == 0 || | |
| 394 this.plugin.desktopHeight == 0) | |
| 395 return; | |
| 396 | |
| 397 var windowWidth = window.innerWidth; | |
| 398 var windowHeight = window.innerHeight; | |
| 399 var scale = 1.0; | |
| 400 | |
| 401 if (remoting.scaleToFit) { | |
| 402 var scaleFitHeight = 1.0 * windowHeight / this.plugin.desktopHeight; | |
| 403 var scaleFitWidth = 1.0 * windowWidth / this.plugin.desktopWidth; | |
| 404 scale = Math.min(1.0, scaleFitHeight, scaleFitWidth); | |
| 405 } | |
| 406 | |
| 407 // Resize the plugin if necessary. | |
| 408 this.plugin.width = this.plugin.desktopWidth * scale; | |
| 409 this.plugin.height = this.plugin.desktopHeight * scale; | |
| 410 | |
| 411 // Position the container. | |
| 412 // TODO(wez): We should take into account scrollbars when positioning. | |
| 413 var parentNode = this.plugin.parentNode; | |
| 414 if (this.plugin.width < windowWidth) | |
| 415 parentNode.style.left = (windowWidth - this.plugin.width) / 2 + 'px'; | |
| 416 else | |
| 417 parentNode.style.left = '0'; | |
| 418 if (this.plugin.height < windowHeight) | |
| 419 parentNode.style.top = (windowHeight - this.plugin.height) / 2 + 'px'; | |
| 420 else | |
| 421 parentNode.style.top = '0'; | |
| 422 | |
| 423 remoting.debug.log('plugin dimensions: ' + | |
| 424 parentNode.style.left + ',' + | |
| 425 parentNode.style.top + '-' + | |
| 426 this.plugin.width + 'x' + this.plugin.height + '.'); | |
| 427 this.plugin.setScaleToFit(remoting.scaleToFit); | |
| 428 }; | |
| 429 | |
| 430 /** | |
| 431 * Returns an associative array with a set of stats for this connection. | |
| 432 * | |
| 433 * @return {Object.<string, number>} The connection statistics. | |
| 434 */ | |
| 435 remoting.ClientSession.prototype.stats = function() { | |
| 436 var dict = {}; | |
| 437 dict[remoting.ClientSession.STATS_KEY_VIDEO_BANDWIDTH] = | |
| 438 this.plugin.videoBandwidth; | |
| 439 dict[remoting.ClientSession.STATS_KEY_VIDEO_FRAME_RATE] = | |
| 440 this.plugin.videoFrameRate; | |
| 441 dict[remoting.ClientSession.STATS_KEY_CAPTURE_LATENCY] = | |
| 442 this.plugin.videoCaptureLatency; | |
| 443 dict[remoting.ClientSession.STATS_KEY_ENCODE_LATENCY] = | |
| 444 this.plugin.videoEncodeLatency; | |
| 445 dict[remoting.ClientSession.STATS_KEY_DECODE_LATENCY] = | |
| 446 this.plugin.videoDecodeLatency; | |
| 447 dict[remoting.ClientSession.STATS_KEY_RENDER_LATENCY] = | |
| 448 this.plugin.videoRenderLatency; | |
| 449 dict[remoting.ClientSession.STATS_KEY_ROUNDTRIP_LATENCY] = | |
| 450 this.plugin.roundTripLatency; | |
| 451 return dict; | |
| 452 }; | |
| 453 | |
| 454 /** | |
| 455 * Logs statistics. | |
| 456 * | |
| 457 * @param {Object.<string, number>} stats | |
| 458 */ | |
| 459 remoting.ClientSession.prototype.logStatistics = function(stats) { | |
| 460 this.logToServer.logStatistics(stats); | |
| 461 }; | |
| OLD | NEW |