| 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 that wraps low-level details of interacting with the client plugin. | |
| 8 * | |
| 9 * This abstracts a <embed> element and controls the plugin which does | |
| 10 * the actual remoting work. It also handles differences between | |
| 11 * client plugins versions when it is necessary. | |
| 12 */ | |
| 13 | |
| 14 'use strict'; | |
| 15 | |
| 16 /** @suppress {duplicate} */ | |
| 17 var remoting = remoting || {}; | |
| 18 | |
| 19 /** @constructor */ | |
| 20 remoting.ClientPluginMessage = function() { | |
| 21 /** @type {string} */ | |
| 22 this.method = ''; | |
| 23 | |
| 24 /** @type {Object<string,*>} */ | |
| 25 this.data = {}; | |
| 26 }; | |
| 27 | |
| 28 /** | |
| 29 * @param {Element} container The container for the embed element. | |
| 30 * @param {Array<string>} requiredCapabilities The set of capabilties that the | |
| 31 * session must support for this application. | |
| 32 * @constructor | |
| 33 * @implements {remoting.ClientPlugin} | |
| 34 */ | |
| 35 remoting.ClientPluginImpl = function(container, | |
| 36 requiredCapabilities) { | |
| 37 // TODO(kelvinp): Hack to remove all plugin elements as our current code does | |
| 38 // not handle connection cancellation properly. | |
| 39 container.innerText = ''; | |
| 40 this.plugin_ = remoting.ClientPluginImpl.createPluginElement_(); | |
| 41 this.plugin_.id = 'session-client-plugin'; | |
| 42 container.appendChild(this.plugin_); | |
| 43 | |
| 44 /** @private {Array<string>} */ | |
| 45 this.requiredCapabilities_ = requiredCapabilities; | |
| 46 | |
| 47 /** @private {remoting.ClientPlugin.ConnectionEventHandler} */ | |
| 48 this.connectionEventHandler_ = null; | |
| 49 | |
| 50 /** @private {?function(string, number, number)} */ | |
| 51 this.updateMouseCursorImage_ = base.doNothing; | |
| 52 /** @private {?function(string, string)} */ | |
| 53 this.updateClipboardData_ = base.doNothing; | |
| 54 /** @private {?function(string)} */ | |
| 55 this.onCastExtensionHandler_ = base.doNothing; | |
| 56 /** @private {?function({rects:Array<Array<number>>}):void} */ | |
| 57 this.debugRegionHandler_ = null; | |
| 58 | |
| 59 /** @private {number} */ | |
| 60 this.pluginApiVersion_ = -1; | |
| 61 /** @private {Array<string>} */ | |
| 62 this.pluginApiFeatures_ = []; | |
| 63 /** @private {number} */ | |
| 64 this.pluginApiMinVersion_ = -1; | |
| 65 /** | |
| 66 * Capabilities to be used for the next connect request. | |
| 67 * @private {!Array<string>} | |
| 68 */ | |
| 69 this.capabilities_ = []; | |
| 70 /** | |
| 71 * Capabilities that are negotiated between the client and the host. | |
| 72 * @private {Array<remoting.ClientSession.Capability>} | |
| 73 */ | |
| 74 this.hostCapabilities_ = null; | |
| 75 /** @private {boolean} */ | |
| 76 this.helloReceived_ = false; | |
| 77 /** @private {function(boolean)|null} */ | |
| 78 this.onInitializedCallback_ = null; | |
| 79 /** @private {function(string, string):void} */ | |
| 80 this.onPairingComplete_ = function(clientId, sharedSecret) {}; | |
| 81 /** @private {remoting.ClientSession.PerfStats} */ | |
| 82 this.perfStats_ = new remoting.ClientSession.PerfStats(); | |
| 83 | |
| 84 /** @type {remoting.ClientPluginImpl} */ | |
| 85 var that = this; | |
| 86 this.plugin_.addEventListener('message', | |
| 87 /** @param {Event} event Message event from the plugin. */ | |
| 88 function(event) { | |
| 89 that.handleMessage_( | |
| 90 /** @type {remoting.ClientPluginMessage} */ (event.data)); | |
| 91 }, false); | |
| 92 | |
| 93 if (remoting.settings.CLIENT_PLUGIN_TYPE == 'native') { | |
| 94 window.setTimeout(this.showPluginForClickToPlay_.bind(this), 500); | |
| 95 } | |
| 96 | |
| 97 /** @private */ | |
| 98 this.hostDesktop_ = new remoting.ClientPlugin.HostDesktopImpl( | |
| 99 this, this.postMessage_.bind(this)); | |
| 100 | |
| 101 /** @private */ | |
| 102 this.extensions_ = new remoting.ProtocolExtensionManager( | |
| 103 this.sendClientMessage_.bind(this)); | |
| 104 | |
| 105 /** @private {remoting.CredentialsProvider} */ | |
| 106 this.credentials_ = null; | |
| 107 | |
| 108 /** @private {string} */ | |
| 109 this.keyRemappings_ = ''; | |
| 110 }; | |
| 111 | |
| 112 /** | |
| 113 * Creates plugin element without adding it to a container. | |
| 114 * | |
| 115 * @return {HTMLEmbedElement} Plugin element | |
| 116 */ | |
| 117 remoting.ClientPluginImpl.createPluginElement_ = function() { | |
| 118 var plugin = | |
| 119 /** @type {HTMLEmbedElement} */ (document.createElement('embed')); | |
| 120 if (remoting.settings.CLIENT_PLUGIN_TYPE == 'pnacl') { | |
| 121 plugin.src = 'remoting_client_pnacl.nmf'; | |
| 122 plugin.type = 'application/x-pnacl'; | |
| 123 } else if (remoting.settings.CLIENT_PLUGIN_TYPE == 'nacl') { | |
| 124 plugin.src = 'remoting_client_nacl.nmf'; | |
| 125 plugin.type = 'application/x-nacl'; | |
| 126 } else { | |
| 127 plugin.src = 'about://none'; | |
| 128 plugin.type = 'application/vnd.chromium.remoting-viewer'; | |
| 129 } | |
| 130 plugin.width = '0'; | |
| 131 plugin.height = '0'; | |
| 132 plugin.tabIndex = 0; // Required, otherwise focus() doesn't work. | |
| 133 return plugin; | |
| 134 } | |
| 135 | |
| 136 /** | |
| 137 * Chromoting session API version (for this javascript). | |
| 138 * This is compared with the plugin API version to verify that they are | |
| 139 * compatible. | |
| 140 * | |
| 141 * @const | |
| 142 * @private | |
| 143 */ | |
| 144 remoting.ClientPluginImpl.prototype.API_VERSION_ = 6; | |
| 145 | |
| 146 /** | |
| 147 * The oldest API version that we support. | |
| 148 * This will differ from the |API_VERSION_| if we maintain backward | |
| 149 * compatibility with older API versions. | |
| 150 * | |
| 151 * @const | |
| 152 * @private | |
| 153 */ | |
| 154 remoting.ClientPluginImpl.prototype.API_MIN_VERSION_ = 5; | |
| 155 | |
| 156 /** | |
| 157 * @param {remoting.ClientPlugin.ConnectionEventHandler} handler | |
| 158 */ | |
| 159 remoting.ClientPluginImpl.prototype.setConnectionEventHandler = | |
| 160 function(handler) { | |
| 161 this.connectionEventHandler_ = handler; | |
| 162 }; | |
| 163 | |
| 164 /** | |
| 165 * @param {function(string, number, number):void} handler | |
| 166 */ | |
| 167 remoting.ClientPluginImpl.prototype.setMouseCursorHandler = function(handler) { | |
| 168 this.updateMouseCursorImage_ = handler; | |
| 169 }; | |
| 170 | |
| 171 /** | |
| 172 * @param {function(string, string):void} handler | |
| 173 */ | |
| 174 remoting.ClientPluginImpl.prototype.setClipboardHandler = function(handler) { | |
| 175 this.updateClipboardData_ = handler; | |
| 176 }; | |
| 177 | |
| 178 /** | |
| 179 * @param {?function({rects:Array<Array<number>>}):void} handler | |
| 180 */ | |
| 181 remoting.ClientPluginImpl.prototype.setDebugDirtyRegionHandler = | |
| 182 function(handler) { | |
| 183 this.debugRegionHandler_ = handler; | |
| 184 this.plugin_.postMessage(JSON.stringify( | |
| 185 { method: 'enableDebugRegion', data: { enable: handler != null } })); | |
| 186 }; | |
| 187 | |
| 188 /** | |
| 189 * @param {string|remoting.ClientPluginMessage} | |
| 190 * rawMessage Message from the plugin. | |
| 191 * @private | |
| 192 */ | |
| 193 remoting.ClientPluginImpl.prototype.handleMessage_ = function(rawMessage) { | |
| 194 var message = | |
| 195 /** @type {remoting.ClientPluginMessage} */ | |
| 196 ((typeof(rawMessage) == 'string') ? base.jsonParseSafe(rawMessage) | |
| 197 : rawMessage); | |
| 198 if (!message || !('method' in message) || !('data' in message)) { | |
| 199 console.error('Received invalid message from the plugin:', rawMessage); | |
| 200 return; | |
| 201 } | |
| 202 | |
| 203 try { | |
| 204 this.handleMessageMethod_(message); | |
| 205 } catch(/** @type {*} */ e) { | |
| 206 console.error(e); | |
| 207 } | |
| 208 } | |
| 209 | |
| 210 /** | |
| 211 * @param {remoting.ClientPluginMessage} | |
| 212 * message Parsed message from the plugin. | |
| 213 * @private | |
| 214 */ | |
| 215 remoting.ClientPluginImpl.prototype.handleMessageMethod_ = function(message) { | |
| 216 /** | |
| 217 * Splits a string into a list of words delimited by spaces. | |
| 218 * @param {string} str String that should be split. | |
| 219 * @return {!Array<string>} List of words. | |
| 220 */ | |
| 221 var tokenize = function(str) { | |
| 222 /** @type {Array<string>} */ | |
| 223 var tokens = str.match(/\S+/g); | |
| 224 return tokens ? tokens : []; | |
| 225 }; | |
| 226 | |
| 227 if (this.connectionEventHandler_) { | |
| 228 var handler = this.connectionEventHandler_; | |
| 229 | |
| 230 if (message.method == 'sendOutgoingIq') { | |
| 231 handler.onOutgoingIq(base.getStringAttr(message.data, 'iq')); | |
| 232 | |
| 233 } else if (message.method == 'logDebugMessage') { | |
| 234 handler.onDebugMessage(base.getStringAttr(message.data, 'message')); | |
| 235 | |
| 236 } else if (message.method == 'onConnectionStatus') { | |
| 237 var stateString = base.getStringAttr(message.data, 'state'); | |
| 238 var state = remoting.ClientSession.State.fromString(stateString); | |
| 239 var error = remoting.ClientSession.ConnectionError.fromString( | |
| 240 base.getStringAttr(message.data, 'error')); | |
| 241 | |
| 242 // Delay firing the CONNECTED event until the capabilities are negotiated, | |
| 243 // TODO(kelvinp): Fix the client plugin to fire capabilities and the | |
| 244 // connected event in the same message. | |
| 245 if (state === remoting.ClientSession.State.CONNECTED) { | |
| 246 base.debug.assert(this.hostCapabilities_ === null, | |
| 247 'Capabilities should only be set after the session is connected'); | |
| 248 return; | |
| 249 } | |
| 250 handler.onConnectionStatusUpdate(state, error); | |
| 251 | |
| 252 } else if (message.method == 'onRouteChanged') { | |
| 253 var channel = base.getStringAttr(message.data, 'channel'); | |
| 254 var connectionType = base.getStringAttr(message.data, 'connectionType'); | |
| 255 handler.onRouteChanged(channel, connectionType); | |
| 256 | |
| 257 } else if (message.method == 'onConnectionReady') { | |
| 258 var ready = base.getBooleanAttr(message.data, 'ready'); | |
| 259 handler.onConnectionReady(ready); | |
| 260 | |
| 261 } else if (message.method == 'setCapabilities') { | |
| 262 var capabilityString = base.getStringAttr(message.data, 'capabilities'); | |
| 263 console.log('plugin: setCapabilities: [' + capabilityString + ']'); | |
| 264 | |
| 265 base.debug.assert(this.hostCapabilities_ === null, | |
| 266 'setCapabilities() should only be called once'); | |
| 267 this.hostCapabilities_ = tokenize(capabilityString); | |
| 268 | |
| 269 handler.onConnectionStatusUpdate( | |
| 270 remoting.ClientSession.State.CONNECTED, | |
| 271 remoting.ClientSession.ConnectionError.NONE); | |
| 272 this.extensions_.start(); | |
| 273 | |
| 274 } else if (message.method == 'onFirstFrameReceived') { | |
| 275 handler.onFirstFrameReceived(); | |
| 276 | |
| 277 } | |
| 278 } | |
| 279 | |
| 280 if (message.method == 'hello') { | |
| 281 // Resize in case we had to enlarge it to support click-to-play. | |
| 282 this.hidePluginForClickToPlay_(); | |
| 283 this.pluginApiVersion_ = base.getNumberAttr(message.data, 'apiVersion'); | |
| 284 this.pluginApiMinVersion_ = | |
| 285 base.getNumberAttr(message.data, 'apiMinVersion'); | |
| 286 | |
| 287 if (this.pluginApiVersion_ >= 7) { | |
| 288 this.pluginApiFeatures_ = | |
| 289 tokenize(base.getStringAttr(message.data, 'apiFeatures')); | |
| 290 | |
| 291 // Negotiate capabilities. | |
| 292 /** @type {!Array<string>} */ | |
| 293 var supportedCapabilities = []; | |
| 294 if ('supportedCapabilities' in message.data) { | |
| 295 supportedCapabilities = | |
| 296 tokenize(base.getStringAttr(message.data, 'supportedCapabilities')); | |
| 297 } | |
| 298 // At the moment the webapp does not recognize any of | |
| 299 // 'requestedCapabilities' capabilities (so they all should be disabled) | |
| 300 // and do not care about any of 'supportedCapabilities' capabilities (so | |
| 301 // they all can be enabled). | |
| 302 // All the required capabilities (specified by the app) are added to this. | |
| 303 this.capabilities_ = supportedCapabilities.concat( | |
| 304 this.requiredCapabilities_); | |
| 305 } else if (this.pluginApiVersion_ >= 6) { | |
| 306 this.pluginApiFeatures_ = ['highQualityScaling', 'injectKeyEvent']; | |
| 307 } else { | |
| 308 this.pluginApiFeatures_ = ['highQualityScaling']; | |
| 309 } | |
| 310 this.helloReceived_ = true; | |
| 311 if (this.onInitializedCallback_ != null) { | |
| 312 this.onInitializedCallback_(true); | |
| 313 this.onInitializedCallback_ = null; | |
| 314 } | |
| 315 | |
| 316 } else if (message.method == 'onDesktopSize') { | |
| 317 this.hostDesktop_.onSizeUpdated(message); | |
| 318 } else if (message.method == 'onDesktopShape') { | |
| 319 this.hostDesktop_.onShapeUpdated(message); | |
| 320 } else if (message.method == 'onPerfStats') { | |
| 321 // Return value is ignored. These calls will throw an error if the value | |
| 322 // is not a number. | |
| 323 base.getNumberAttr(message.data, 'videoBandwidth'); | |
| 324 base.getNumberAttr(message.data, 'videoFrameRate'); | |
| 325 base.getNumberAttr(message.data, 'captureLatency'); | |
| 326 base.getNumberAttr(message.data, 'encodeLatency'); | |
| 327 base.getNumberAttr(message.data, 'decodeLatency'); | |
| 328 base.getNumberAttr(message.data, 'renderLatency'); | |
| 329 base.getNumberAttr(message.data, 'roundtripLatency'); | |
| 330 this.perfStats_ = | |
| 331 /** @type {remoting.ClientSession.PerfStats} */ (message.data); | |
| 332 | |
| 333 } else if (message.method == 'injectClipboardItem') { | |
| 334 var mimetype = base.getStringAttr(message.data, 'mimeType'); | |
| 335 var item = base.getStringAttr(message.data, 'item'); | |
| 336 this.updateClipboardData_(mimetype, item); | |
| 337 | |
| 338 } else if (message.method == 'fetchPin') { | |
| 339 // The pairingSupported value in the dictionary indicates whether both | |
| 340 // client and host support pairing. If the client doesn't support pairing, | |
| 341 // then the value won't be there at all, so give it a default of false. | |
| 342 var pairingSupported = base.getBooleanAttr(message.data, 'pairingSupported', | |
| 343 false); | |
| 344 this.credentials_.getPIN(pairingSupported).then( | |
| 345 this.onPinFetched_.bind(this) | |
| 346 ); | |
| 347 | |
| 348 } else if (message.method == 'fetchThirdPartyToken') { | |
| 349 var tokenUrl = base.getStringAttr(message.data, 'tokenUrl'); | |
| 350 var hostPublicKey = base.getStringAttr(message.data, 'hostPublicKey'); | |
| 351 var scope = base.getStringAttr(message.data, 'scope'); | |
| 352 this.credentials_.getThirdPartyToken(tokenUrl, hostPublicKey, scope).then( | |
| 353 this.onThirdPartyTokenFetched_.bind(this) | |
| 354 ); | |
| 355 } else if (message.method == 'pairingResponse') { | |
| 356 var clientId = base.getStringAttr(message.data, 'clientId'); | |
| 357 var sharedSecret = base.getStringAttr(message.data, 'sharedSecret'); | |
| 358 this.onPairingComplete_(clientId, sharedSecret); | |
| 359 | |
| 360 } else if (message.method == 'unsetCursorShape') { | |
| 361 this.updateMouseCursorImage_('', 0, 0); | |
| 362 | |
| 363 } else if (message.method == 'setCursorShape') { | |
| 364 var width = base.getNumberAttr(message.data, 'width'); | |
| 365 var height = base.getNumberAttr(message.data, 'height'); | |
| 366 var hotspotX = base.getNumberAttr(message.data, 'hotspotX'); | |
| 367 var hotspotY = base.getNumberAttr(message.data, 'hotspotY'); | |
| 368 var srcArrayBuffer = base.getObjectAttr(message.data, 'data'); | |
| 369 | |
| 370 var canvas = | |
| 371 /** @type {HTMLCanvasElement} */ (document.createElement('canvas')); | |
| 372 canvas.width = width; | |
| 373 canvas.height = height; | |
| 374 | |
| 375 var context = | |
| 376 /** @type {CanvasRenderingContext2D} */ (canvas.getContext('2d')); | |
| 377 var imageData = context.getImageData(0, 0, width, height); | |
| 378 base.debug.assert(srcArrayBuffer instanceof ArrayBuffer); | |
| 379 var src = new Uint8Array(/** @type {ArrayBuffer} */(srcArrayBuffer)); | |
| 380 var dest = imageData.data; | |
| 381 for (var i = 0; i < /** @type {number} */(dest.length); i += 4) { | |
| 382 dest[i] = src[i + 2]; | |
| 383 dest[i + 1] = src[i + 1]; | |
| 384 dest[i + 2] = src[i]; | |
| 385 dest[i + 3] = src[i + 3]; | |
| 386 } | |
| 387 | |
| 388 context.putImageData(imageData, 0, 0); | |
| 389 this.updateMouseCursorImage_(canvas.toDataURL(), hotspotX, hotspotY); | |
| 390 | |
| 391 } else if (message.method == 'onDebugRegion') { | |
| 392 if (this.debugRegionHandler_) { | |
| 393 this.debugRegionHandler_( | |
| 394 /** @type {{rects: Array<(Array<number>)>}} **/(message.data)); | |
| 395 } | |
| 396 } else if (message.method == 'extensionMessage') { | |
| 397 var extMsgType = base.getStringAttr(message.data, 'type'); | |
| 398 var extMsgData = base.getStringAttr(message.data, 'data'); | |
| 399 this.extensions_.onProtocolExtensionMessage(extMsgType, extMsgData); | |
| 400 | |
| 401 } | |
| 402 }; | |
| 403 | |
| 404 /** | |
| 405 * Deletes the plugin. | |
| 406 */ | |
| 407 remoting.ClientPluginImpl.prototype.dispose = function() { | |
| 408 if (this.plugin_) { | |
| 409 this.plugin_.parentNode.removeChild(this.plugin_); | |
| 410 this.plugin_ = null; | |
| 411 } | |
| 412 | |
| 413 base.dispose(this.extensions_); | |
| 414 this.extensions_ = null; | |
| 415 }; | |
| 416 | |
| 417 /** | |
| 418 * @return {HTMLEmbedElement} HTML element that corresponds to the plugin. | |
| 419 */ | |
| 420 remoting.ClientPluginImpl.prototype.element = function() { | |
| 421 return this.plugin_; | |
| 422 }; | |
| 423 | |
| 424 /** | |
| 425 * @param {function(boolean): void} onDone | |
| 426 */ | |
| 427 remoting.ClientPluginImpl.prototype.initialize = function(onDone) { | |
| 428 if (this.helloReceived_) { | |
| 429 onDone(true); | |
| 430 } else { | |
| 431 this.onInitializedCallback_ = onDone; | |
| 432 } | |
| 433 }; | |
| 434 | |
| 435 /** | |
| 436 * @return {boolean} True if the plugin and web-app versions are compatible. | |
| 437 */ | |
| 438 remoting.ClientPluginImpl.prototype.isSupportedVersion = function() { | |
| 439 if (!this.helloReceived_) { | |
| 440 console.error( | |
| 441 "isSupportedVersion() is called before the plugin is initialized."); | |
| 442 return false; | |
| 443 } | |
| 444 return this.API_VERSION_ >= this.pluginApiMinVersion_ && | |
| 445 this.pluginApiVersion_ >= this.API_MIN_VERSION_; | |
| 446 }; | |
| 447 | |
| 448 /** | |
| 449 * @param {remoting.ClientPlugin.Feature} feature The feature to test for. | |
| 450 * @return {boolean} True if the plugin supports the named feature. | |
| 451 */ | |
| 452 remoting.ClientPluginImpl.prototype.hasFeature = function(feature) { | |
| 453 if (!this.helloReceived_) { | |
| 454 console.error( | |
| 455 "hasFeature() is called before the plugin is initialized."); | |
| 456 return false; | |
| 457 } | |
| 458 return this.pluginApiFeatures_.indexOf(feature) > -1; | |
| 459 }; | |
| 460 | |
| 461 | |
| 462 /** | |
| 463 * @param {remoting.ClientSession.Capability} capability The capability to test | |
| 464 * for. | |
| 465 * @return {boolean} True if the capability has been negotiated between | |
| 466 * the client and host. | |
| 467 */ | |
| 468 remoting.ClientPluginImpl.prototype.hasCapability = function(capability) { | |
| 469 return this.hostCapabilities_ !== null && | |
| 470 this.hostCapabilities_.indexOf(capability) > -1; | |
| 471 }; | |
| 472 | |
| 473 /** | |
| 474 * @return {boolean} True if the plugin supports the injectKeyEvent API. | |
| 475 */ | |
| 476 remoting.ClientPluginImpl.prototype.isInjectKeyEventSupported = function() { | |
| 477 return this.pluginApiVersion_ >= 6; | |
| 478 }; | |
| 479 | |
| 480 /** | |
| 481 * @param {string} iq Incoming IQ stanza. | |
| 482 */ | |
| 483 remoting.ClientPluginImpl.prototype.onIncomingIq = function(iq) { | |
| 484 if (this.plugin_ && this.plugin_.postMessage) { | |
| 485 this.plugin_.postMessage(JSON.stringify( | |
| 486 { method: 'incomingIq', data: { iq: iq } })); | |
| 487 } else { | |
| 488 // plugin.onIq may not be set after the plugin has been shut | |
| 489 // down. Particularly this happens when we receive response to | |
| 490 // session-terminate stanza. | |
| 491 console.warn('plugin.onIq is not set so dropping incoming message.'); | |
| 492 } | |
| 493 }; | |
| 494 | |
| 495 /** | |
| 496 * @param {remoting.Host} host The host to connect to. | |
| 497 * @param {string} localJid Local jid. | |
| 498 * @param {remoting.CredentialsProvider} credentialsProvider | |
| 499 */ | |
| 500 remoting.ClientPluginImpl.prototype.connect = | |
| 501 function(host, localJid, credentialsProvider) { | |
| 502 var keyFilter = ''; | |
| 503 if (remoting.platformIsMac()) { | |
| 504 keyFilter = 'mac'; | |
| 505 } else if (remoting.platformIsChromeOS()) { | |
| 506 keyFilter = 'cros'; | |
| 507 } | |
| 508 | |
| 509 // Use PPB_VideoDecoder API only in Chrome 43 and above. It is broken in | |
| 510 // previous versions of Chrome, see crbug.com/459103 and crbug.com/463577 . | |
| 511 var enableVideoDecodeRenderer = | |
| 512 parseInt((remoting.getChromeVersion() || '0').split('.')[0], 10) >= 43; | |
| 513 this.plugin_.postMessage(JSON.stringify( | |
| 514 { method: 'delegateLargeCursors', data: {} })); | |
| 515 var methods = 'third_party,spake2_pair,spake2_hmac,spake2_plain'; | |
| 516 this.credentials_ = credentialsProvider; | |
| 517 this.useAsyncPinDialog_(); | |
| 518 this.plugin_.postMessage(JSON.stringify( | |
| 519 { method: 'connect', data: { | |
| 520 hostJid: host.jabberId, | |
| 521 hostPublicKey: host.publicKey, | |
| 522 localJid: localJid, | |
| 523 sharedSecret: '', | |
| 524 authenticationMethods: methods, | |
| 525 authenticationTag: host.hostId, | |
| 526 capabilities: this.capabilities_.join(" "), | |
| 527 clientPairingId: credentialsProvider.getPairingInfo().clientId, | |
| 528 clientPairedSecret: credentialsProvider.getPairingInfo().sharedSecret, | |
| 529 keyFilter: keyFilter, | |
| 530 enableVideoDecodeRenderer: enableVideoDecodeRenderer | |
| 531 } | |
| 532 })); | |
| 533 }; | |
| 534 | |
| 535 /** | |
| 536 * Release all currently pressed keys. | |
| 537 */ | |
| 538 remoting.ClientPluginImpl.prototype.releaseAllKeys = function() { | |
| 539 this.plugin_.postMessage(JSON.stringify( | |
| 540 { method: 'releaseAllKeys', data: {} })); | |
| 541 }; | |
| 542 | |
| 543 /** | |
| 544 * Sets and stores the key remapping setting for the current host. | |
| 545 * | |
| 546 * @param {string} remappings Comma separated list of key remappings. | |
| 547 */ | |
| 548 remoting.ClientPluginImpl.prototype.setRemapKeys = | |
| 549 function(remappings) { | |
| 550 // Cancel any existing remappings and apply the new ones. | |
| 551 this.applyRemapKeys_(this.keyRemappings_, false); | |
| 552 this.applyRemapKeys_(remappings, true); | |
| 553 this.keyRemappings_ = remappings; | |
| 554 }; | |
| 555 | |
| 556 /** | |
| 557 * Applies the configured key remappings to the session, or resets them. | |
| 558 * | |
| 559 * @param {string} remapKeys | |
| 560 * @param {boolean} apply True to apply remappings, false to cancel them. | |
| 561 * @private | |
| 562 */ | |
| 563 remoting.ClientPluginImpl.prototype.applyRemapKeys_ = | |
| 564 function(remapKeys, apply) { | |
| 565 if (remapKeys == '') { | |
| 566 return; | |
| 567 } | |
| 568 | |
| 569 var remappings = remapKeys.split(','); | |
| 570 for (var i = 0; i < remappings.length; ++i) { | |
| 571 var keyCodes = remappings[i].split('>'); | |
| 572 if (keyCodes.length != 2) { | |
| 573 console.log('bad remapKey: ' + remappings[i]); | |
| 574 continue; | |
| 575 } | |
| 576 var fromKey = parseInt(keyCodes[0], 0); | |
| 577 var toKey = parseInt(keyCodes[1], 0); | |
| 578 if (!fromKey || !toKey) { | |
| 579 console.log('bad remapKey code: ' + remappings[i]); | |
| 580 continue; | |
| 581 } | |
| 582 if (apply) { | |
| 583 console.log('remapKey 0x' + fromKey.toString(16) + | |
| 584 '>0x' + toKey.toString(16)); | |
| 585 this.remapKey(fromKey, toKey); | |
| 586 } else { | |
| 587 console.log('cancel remapKey 0x' + fromKey.toString(16)); | |
| 588 this.remapKey(fromKey, fromKey); | |
| 589 } | |
| 590 } | |
| 591 }; | |
| 592 | |
| 593 /** | |
| 594 * Sends a key combination to the remoting host, by sending down events for | |
| 595 * the given keys, followed by up events in reverse order. | |
| 596 * | |
| 597 * @param {Array<number>} keys Key codes to be sent. | |
| 598 * @return {void} Nothing. | |
| 599 */ | |
| 600 remoting.ClientPluginImpl.prototype.injectKeyCombination = | |
| 601 function(keys) { | |
| 602 for (var i = 0; i < keys.length; i++) { | |
| 603 this.injectKeyEvent(keys[i], true); | |
| 604 } | |
| 605 for (var i = 0; i < keys.length; i++) { | |
| 606 this.injectKeyEvent(keys[i], false); | |
| 607 } | |
| 608 }; | |
| 609 | |
| 610 /** | |
| 611 * Send a key event to the host. | |
| 612 * | |
| 613 * @param {number} usbKeycode The USB-style code of the key to inject. | |
| 614 * @param {boolean} pressed True to inject a key press, False for a release. | |
| 615 */ | |
| 616 remoting.ClientPluginImpl.prototype.injectKeyEvent = | |
| 617 function(usbKeycode, pressed) { | |
| 618 this.plugin_.postMessage(JSON.stringify( | |
| 619 { method: 'injectKeyEvent', data: { | |
| 620 'usbKeycode': usbKeycode, | |
| 621 'pressed': pressed} | |
| 622 })); | |
| 623 }; | |
| 624 | |
| 625 /** | |
| 626 * Remap one USB keycode to another in all subsequent key events. | |
| 627 * | |
| 628 * @param {number} fromKeycode The USB-style code of the key to remap. | |
| 629 * @param {number} toKeycode The USB-style code to remap the key to. | |
| 630 */ | |
| 631 remoting.ClientPluginImpl.prototype.remapKey = | |
| 632 function(fromKeycode, toKeycode) { | |
| 633 this.plugin_.postMessage(JSON.stringify( | |
| 634 { method: 'remapKey', data: { | |
| 635 'fromKeycode': fromKeycode, | |
| 636 'toKeycode': toKeycode} | |
| 637 })); | |
| 638 }; | |
| 639 | |
| 640 /** | |
| 641 * Enable/disable redirection of the specified key to the web-app. | |
| 642 * | |
| 643 * @param {number} keycode The USB-style code of the key. | |
| 644 * @param {Boolean} trap True to enable trapping, False to disable. | |
| 645 */ | |
| 646 remoting.ClientPluginImpl.prototype.trapKey = function(keycode, trap) { | |
| 647 this.plugin_.postMessage(JSON.stringify( | |
| 648 { method: 'trapKey', data: { | |
| 649 'keycode': keycode, | |
| 650 'trap': trap} | |
| 651 })); | |
| 652 }; | |
| 653 | |
| 654 /** | |
| 655 * Returns an associative array with a set of stats for this connecton. | |
| 656 * | |
| 657 * @return {remoting.ClientSession.PerfStats} The connection statistics. | |
| 658 */ | |
| 659 remoting.ClientPluginImpl.prototype.getPerfStats = function() { | |
| 660 return this.perfStats_; | |
| 661 }; | |
| 662 | |
| 663 /** | |
| 664 * Sends a clipboard item to the host. | |
| 665 * | |
| 666 * @param {string} mimeType The MIME type of the clipboard item. | |
| 667 * @param {string} item The clipboard item. | |
| 668 */ | |
| 669 remoting.ClientPluginImpl.prototype.sendClipboardItem = | |
| 670 function(mimeType, item) { | |
| 671 if (!this.hasFeature(remoting.ClientPlugin.Feature.SEND_CLIPBOARD_ITEM)) | |
| 672 return; | |
| 673 this.plugin_.postMessage(JSON.stringify( | |
| 674 { method: 'sendClipboardItem', | |
| 675 data: { mimeType: mimeType, item: item }})); | |
| 676 }; | |
| 677 | |
| 678 /** | |
| 679 * Notifies the host that the client has the specified size and pixel density. | |
| 680 * | |
| 681 * @param {number} width The available client width in DIPs. | |
| 682 * @param {number} height The available client height in DIPs. | |
| 683 * @param {number} device_scale The number of device pixels per DIP. | |
| 684 */ | |
| 685 remoting.ClientPluginImpl.prototype.notifyClientResolution = | |
| 686 function(width, height, device_scale) { | |
| 687 this.hostDesktop_.resize(width, height, device_scale); | |
| 688 }; | |
| 689 | |
| 690 /** | |
| 691 * Requests that the host pause or resume sending video updates. | |
| 692 * | |
| 693 * @param {boolean} pause True to suspend video updates, false otherwise. | |
| 694 */ | |
| 695 remoting.ClientPluginImpl.prototype.pauseVideo = | |
| 696 function(pause) { | |
| 697 if (this.hasFeature(remoting.ClientPlugin.Feature.VIDEO_CONTROL)) { | |
| 698 this.plugin_.postMessage(JSON.stringify( | |
| 699 { method: 'videoControl', data: { pause: pause }})); | |
| 700 } else if (this.hasFeature(remoting.ClientPlugin.Feature.PAUSE_VIDEO)) { | |
| 701 this.plugin_.postMessage(JSON.stringify( | |
| 702 { method: 'pauseVideo', data: { pause: pause }})); | |
| 703 } | |
| 704 }; | |
| 705 | |
| 706 /** | |
| 707 * Requests that the host pause or resume sending audio updates. | |
| 708 * | |
| 709 * @param {boolean} pause True to suspend audio updates, false otherwise. | |
| 710 */ | |
| 711 remoting.ClientPluginImpl.prototype.pauseAudio = | |
| 712 function(pause) { | |
| 713 if (!this.hasFeature(remoting.ClientPlugin.Feature.PAUSE_AUDIO)) { | |
| 714 return; | |
| 715 } | |
| 716 this.plugin_.postMessage(JSON.stringify( | |
| 717 { method: 'pauseAudio', data: { pause: pause }})); | |
| 718 }; | |
| 719 | |
| 720 /** | |
| 721 * Requests that the host configure the video codec for lossless encode. | |
| 722 * | |
| 723 * @param {boolean} wantLossless True to request lossless encoding. | |
| 724 */ | |
| 725 remoting.ClientPluginImpl.prototype.setLosslessEncode = | |
| 726 function(wantLossless) { | |
| 727 if (!this.hasFeature(remoting.ClientPlugin.Feature.VIDEO_CONTROL)) { | |
| 728 return; | |
| 729 } | |
| 730 this.plugin_.postMessage(JSON.stringify( | |
| 731 { method: 'videoControl', data: { losslessEncode: wantLossless }})); | |
| 732 }; | |
| 733 | |
| 734 /** | |
| 735 * Requests that the host configure the video codec for lossless color. | |
| 736 * | |
| 737 * @param {boolean} wantLossless True to request lossless color. | |
| 738 */ | |
| 739 remoting.ClientPluginImpl.prototype.setLosslessColor = | |
| 740 function(wantLossless) { | |
| 741 if (!this.hasFeature(remoting.ClientPlugin.Feature.VIDEO_CONTROL)) { | |
| 742 return; | |
| 743 } | |
| 744 this.plugin_.postMessage(JSON.stringify( | |
| 745 { method: 'videoControl', data: { losslessColor: wantLossless }})); | |
| 746 }; | |
| 747 | |
| 748 /** | |
| 749 * Called when a PIN is obtained from the user. | |
| 750 * | |
| 751 * @param {string} pin The PIN. | |
| 752 * @private | |
| 753 */ | |
| 754 remoting.ClientPluginImpl.prototype.onPinFetched_ = | |
| 755 function(pin) { | |
| 756 if (!this.hasFeature(remoting.ClientPlugin.Feature.ASYNC_PIN)) { | |
| 757 return; | |
| 758 } | |
| 759 this.plugin_.postMessage(JSON.stringify( | |
| 760 { method: 'onPinFetched', data: { pin: pin }})); | |
| 761 }; | |
| 762 | |
| 763 /** | |
| 764 * Tells the plugin to ask for the PIN asynchronously. | |
| 765 * @private | |
| 766 */ | |
| 767 remoting.ClientPluginImpl.prototype.useAsyncPinDialog_ = | |
| 768 function() { | |
| 769 if (!this.hasFeature(remoting.ClientPlugin.Feature.ASYNC_PIN)) { | |
| 770 return; | |
| 771 } | |
| 772 this.plugin_.postMessage(JSON.stringify( | |
| 773 { method: 'useAsyncPinDialog', data: {} })); | |
| 774 }; | |
| 775 | |
| 776 /** | |
| 777 * Allows automatic mouse-lock. | |
| 778 */ | |
| 779 remoting.ClientPluginImpl.prototype.allowMouseLock = function() { | |
| 780 this.plugin_.postMessage(JSON.stringify( | |
| 781 { method: 'allowMouseLock', data: {} })); | |
| 782 }; | |
| 783 | |
| 784 /** | |
| 785 * Sets the third party authentication token and shared secret. | |
| 786 * | |
| 787 * @param {remoting.ThirdPartyToken} token | |
| 788 * @private | |
| 789 */ | |
| 790 remoting.ClientPluginImpl.prototype.onThirdPartyTokenFetched_ = function( | |
| 791 token) { | |
| 792 this.plugin_.postMessage(JSON.stringify( | |
| 793 { method: 'onThirdPartyTokenFetched', | |
| 794 data: { token: token.token, sharedSecret: token.secret}})); | |
| 795 }; | |
| 796 | |
| 797 /** | |
| 798 * Request pairing with the host for PIN-less authentication. | |
| 799 * | |
| 800 * @param {string} clientName The human-readable name of the client. | |
| 801 * @param {function(string, string):void} onDone, Callback to receive the | |
| 802 * client id and shared secret when they are available. | |
| 803 */ | |
| 804 remoting.ClientPluginImpl.prototype.requestPairing = | |
| 805 function(clientName, onDone) { | |
| 806 if (!this.hasFeature(remoting.ClientPlugin.Feature.PINLESS_AUTH)) { | |
| 807 return; | |
| 808 } | |
| 809 this.onPairingComplete_ = onDone; | |
| 810 this.plugin_.postMessage(JSON.stringify( | |
| 811 { method: 'requestPairing', data: { clientName: clientName } })); | |
| 812 }; | |
| 813 | |
| 814 /** | |
| 815 * Send an extension message to the host. | |
| 816 * | |
| 817 * @param {string} type The message type. | |
| 818 * @param {string} message The message payload. | |
| 819 * @private | |
| 820 */ | |
| 821 remoting.ClientPluginImpl.prototype.sendClientMessage_ = | |
| 822 function(type, message) { | |
| 823 if (!this.hasFeature(remoting.ClientPlugin.Feature.EXTENSION_MESSAGE)) { | |
| 824 return; | |
| 825 } | |
| 826 this.plugin_.postMessage(JSON.stringify( | |
| 827 { method: 'extensionMessage', | |
| 828 data: { type: type, data: message } })); | |
| 829 | |
| 830 }; | |
| 831 | |
| 832 remoting.ClientPluginImpl.prototype.hostDesktop = function() { | |
| 833 return this.hostDesktop_; | |
| 834 }; | |
| 835 | |
| 836 remoting.ClientPluginImpl.prototype.extensions = function() { | |
| 837 return this.extensions_; | |
| 838 }; | |
| 839 | |
| 840 /** | |
| 841 * If we haven't yet received a "hello" message from the plugin, change its | |
| 842 * size so that the user can confirm it if click-to-play is enabled, or can | |
| 843 * see the "this plugin is disabled" message if it is actually disabled. | |
| 844 * @private | |
| 845 */ | |
| 846 remoting.ClientPluginImpl.prototype.showPluginForClickToPlay_ = function() { | |
| 847 if (!this.helloReceived_) { | |
| 848 var width = 200; | |
| 849 var height = 200; | |
| 850 this.plugin_.style.width = width + 'px'; | |
| 851 this.plugin_.style.height = height + 'px'; | |
| 852 // Center the plugin just underneath the "Connnecting..." dialog. | |
| 853 var dialog = document.getElementById('client-dialog'); | |
| 854 var dialogRect = dialog.getBoundingClientRect(); | |
| 855 this.plugin_.style.top = (dialogRect.bottom + 16) + 'px'; | |
| 856 this.plugin_.style.left = (window.innerWidth - width) / 2 + 'px'; | |
| 857 this.plugin_.style.position = 'fixed'; | |
| 858 } | |
| 859 }; | |
| 860 | |
| 861 /** | |
| 862 * Undo the CSS rules needed to make the plugin clickable for click-to-play. | |
| 863 * @private | |
| 864 */ | |
| 865 remoting.ClientPluginImpl.prototype.hidePluginForClickToPlay_ = function() { | |
| 866 this.plugin_.style.width = ''; | |
| 867 this.plugin_.style.height = ''; | |
| 868 this.plugin_.style.top = ''; | |
| 869 this.plugin_.style.left = ''; | |
| 870 this.plugin_.style.position = ''; | |
| 871 }; | |
| 872 | |
| 873 /** | |
| 874 * Callback passed to submodules to post a message to the plugin. | |
| 875 * | |
| 876 * @param {Object} message | |
| 877 * @private | |
| 878 */ | |
| 879 remoting.ClientPluginImpl.prototype.postMessage_ = function(message) { | |
| 880 if (this.plugin_ && this.plugin_.postMessage) { | |
| 881 this.plugin_.postMessage(JSON.stringify(message)); | |
| 882 } | |
| 883 }; | |
| 884 | |
| 885 /** | |
| 886 * @constructor | |
| 887 * @implements {remoting.ClientPluginFactory} | |
| 888 */ | |
| 889 remoting.DefaultClientPluginFactory = function() {}; | |
| 890 | |
| 891 /** | |
| 892 * @param {Element} container | |
| 893 * @param {Array<string>} requiredCapabilities | |
| 894 * @return {remoting.ClientPlugin} | |
| 895 */ | |
| 896 remoting.DefaultClientPluginFactory.prototype.createPlugin = | |
| 897 function(container, requiredCapabilities) { | |
| 898 return new remoting.ClientPluginImpl(container, | |
| 899 requiredCapabilities); | |
| 900 }; | |
| 901 | |
| 902 remoting.DefaultClientPluginFactory.prototype.preloadPlugin = function() { | |
| 903 if (remoting.settings.CLIENT_PLUGIN_TYPE != 'pnacl') { | |
| 904 return; | |
| 905 } | |
| 906 | |
| 907 var plugin = remoting.ClientPluginImpl.createPluginElement_(); | |
| 908 plugin.addEventListener( | |
| 909 'loadend', function() { document.body.removeChild(plugin); }, false); | |
| 910 document.body.appendChild(plugin); | |
| 911 }; | |
| OLD | NEW |