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