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 /** 20 * @param {remoting.ViewerPlugin} plugin The plugin 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 */ 26 remoting.ClientPlugin = function(plugin, onExtensionMessage) { 27 this.plugin = plugin; 28 this.onExtensionMessage_ = onExtensionMessage; 29 30 this.desktopWidth = 0; 31 this.desktopHeight = 0; 32 this.desktopXDpi = 96; 33 this.desktopYDpi = 96; 34 35 /** @param {string} iq The Iq stanza received from the host. */ 36 this.onOutgoingIqHandler = function (iq) {}; 37 /** @param {string} message Log message. */ 38 this.onDebugMessageHandler = function (message) {}; 39 /** 40 * @param {number} state The connection state. 41 * @param {number} error The error code, if any. 42 */ 43 this.onConnectionStatusUpdateHandler = function(state, error) {}; 44 /** @param {boolean} ready Connection ready state. */ 45 this.onConnectionReadyHandler = function(ready) {}; 46 47 /** 48 * @param {string} tokenUrl Token-request URL, received from the host. 49 * @param {string} hostPublicKey Public key for the host. 50 * @param {string} scope OAuth scope to request the token for. 51 */ 52 this.fetchThirdPartyTokenHandler = function( 53 tokenUrl, hostPublicKey, scope) {}; 54 this.onDesktopSizeUpdateHandler = function () {}; 55 /** @param {!Array.<string>} capabilities The negotiated capabilities. */ 56 this.onSetCapabilitiesHandler = function (capabilities) {}; 57 this.fetchPinHandler = function (supportsPairing) {}; 58 /** @param {string} data Remote gnubbyd data. */ 59 this.onGnubbyAuthHandler = function(data) {}; 60 /** 61 * @param {string} url 62 * @param {number} hotspotX 63 * @param {number} hotspotY 64 */ 65 this.updateMouseCursorImage = function(url, hotspotX, hotspotY) {}; 66 67 /** @type {remoting.MediaSourceRenderer} */ 68 this.mediaSourceRenderer_ = null; 69 70 /** @type {number} */ 71 this.pluginApiVersion_ = -1; 72 /** @type {Array.<string>} */ 73 this.pluginApiFeatures_ = []; 74 /** @type {number} */ 75 this.pluginApiMinVersion_ = -1; 76 /** @type {!Array.<string>} */ 77 this.capabilities_ = []; 78 /** @type {boolean} */ 79 this.helloReceived_ = false; 80 /** @type {function(boolean)|null} */ 81 this.onInitializedCallback_ = null; 82 /** @type {function(string, string):void} */ 83 this.onPairingComplete_ = function(clientId, sharedSecret) {}; 84 /** @type {remoting.ClientSession.PerfStats} */ 85 this.perfStats_ = new remoting.ClientSession.PerfStats(); 86 87 /** @type {remoting.ClientPlugin} */ 88 var that = this; 89 /** @param {Event} event Message event from the plugin. */ 90 this.plugin.addEventListener('message', function(event) { 91 that.handleMessage_(event.data); 92 }, false); 93 94 if (remoting.settings.CLIENT_PLUGIN_TYPE == 'native') { 95 window.setTimeout(this.showPluginForClickToPlay_.bind(this), 500); 96 } 97 }; 98 99 /** 100 * Set of features for which hasFeature() can be used to test. 101 * 102 * @enum {string} 103 */ 104 remoting.ClientPlugin.Feature = { 105 INJECT_KEY_EVENT: 'injectKeyEvent', 106 NOTIFY_CLIENT_RESOLUTION: 'notifyClientResolution', 107 ASYNC_PIN: 'asyncPin', 108 PAUSE_VIDEO: 'pauseVideo', 109 PAUSE_AUDIO: 'pauseAudio', 110 REMAP_KEY: 'remapKey', 111 SEND_CLIPBOARD_ITEM: 'sendClipboardItem', 112 THIRD_PARTY_AUTH: 'thirdPartyAuth', 113 TRAP_KEY: 'trapKey', 114 PINLESS_AUTH: 'pinlessAuth', 115 EXTENSION_MESSAGE: 'extensionMessage', 116 MEDIA_SOURCE_RENDERING: 'mediaSourceRendering', 117 VIDEO_CONTROL: 'videoControl' 118 }; 119 120 /** 121 * Chromoting session API version (for this javascript). 122 * This is compared with the plugin API version to verify that they are 123 * compatible. 124 * 125 * @const 126 * @private 127 */ 128 remoting.ClientPlugin.prototype.API_VERSION_ = 6; 129 130 /** 131 * The oldest API version that we support. 132 * This will differ from the |API_VERSION_| if we maintain backward 133 * compatibility with older API versions. 134 * 135 * @const 136 * @private 137 */ 138 remoting.ClientPlugin.prototype.API_MIN_VERSION_ = 5; 139 140 /** 141 * @param {string|{method:string, data:Object.<string,*>}} 142 * rawMessage Message from the plugin. 143 * @private 144 */ 145 remoting.ClientPlugin.prototype.handleMessage_ = function(rawMessage) { 146 var message = 147 /** @type {{method:string, data:Object.<string,*>}} */ 148 ((typeof(rawMessage) == 'string') ? jsonParseSafe(rawMessage) 149 : rawMessage); 150 if (!message || !('method' in message) || !('data' in message)) { 151 console.error('Received invalid message from the plugin:', rawMessage); 152 return; 153 } 154 155 try { 156 this.handleMessageMethod_(message); 157 } catch(e) { 158 console.error(/** @type {*} */ (e)); 159 } 160 } 161 162 /** 163 * @param {{method:string, data:Object.<string,*>}} 164 * message Parsed message from the plugin. 165 * @private 166 */ 167 remoting.ClientPlugin.prototype.handleMessageMethod_ = function(message) { 168 /** 169 * Splits a string into a list of words delimited by spaces. 170 * @param {string} str String that should be split. 171 * @return {!Array.<string>} List of words. 172 */ 173 var tokenize = function(str) { 174 /** @type {Array.<string>} */ 175 var tokens = str.match(/\S+/g); 176 return tokens ? tokens : []; 177 }; 178 179 if (message.method == 'hello') { 180 // Resize in case we had to enlarge it to support click-to-play. 181 this.hidePluginForClickToPlay_(); 182 this.pluginApiVersion_ = getNumberAttr(message.data, 'apiVersion'); 183 this.pluginApiMinVersion_ = getNumberAttr(message.data, 'apiMinVersion'); 184 185 if (this.pluginApiVersion_ >= 7) { 186 this.pluginApiFeatures_ = 187 tokenize(getStringAttr(message.data, 'apiFeatures')); 188 189 // Negotiate capabilities. 190 191 /** @type {!Array.<string>} */ 192 var requestedCapabilities = []; 193 if ('requestedCapabilities' in message.data) { 194 requestedCapabilities = 195 tokenize(getStringAttr(message.data, 'requestedCapabilities')); 196 } 197 198 /** @type {!Array.<string>} */ 199 var supportedCapabilities = []; 200 if ('supportedCapabilities' in message.data) { 201 supportedCapabilities = 202 tokenize(getStringAttr(message.data, 'supportedCapabilities')); 203 } 204 205 // At the moment the webapp does not recognize any of 206 // 'requestedCapabilities' capabilities (so they all should be disabled) 207 // and do not care about any of 'supportedCapabilities' capabilities (so 208 // they all can be enabled). 209 this.capabilities_ = supportedCapabilities; 210 211 // Let the host know that the webapp can be requested to always send 212 // the client's dimensions. 213 this.capabilities_.push( 214 remoting.ClientSession.Capability.SEND_INITIAL_RESOLUTION); 215 216 // Let the host know that we're interested in knowing whether or not 217 // it rate-limits desktop-resize requests. 218 this.capabilities_.push( 219 remoting.ClientSession.Capability.RATE_LIMIT_RESIZE_REQUESTS); 220 } else if (this.pluginApiVersion_ >= 6) { 221 this.pluginApiFeatures_ = ['highQualityScaling', 'injectKeyEvent']; 222 } else { 223 this.pluginApiFeatures_ = ['highQualityScaling']; 224 } 225 this.helloReceived_ = true; 226 if (this.onInitializedCallback_ != null) { 227 this.onInitializedCallback_(true); 228 this.onInitializedCallback_ = null; 229 } 230 231 } else if (message.method == 'sendOutgoingIq') { 232 this.onOutgoingIqHandler(getStringAttr(message.data, 'iq')); 233 234 } else if (message.method == 'logDebugMessage') { 235 this.onDebugMessageHandler(getStringAttr(message.data, 'message')); 236 237 } else if (message.method == 'onConnectionStatus') { 238 var state = remoting.ClientSession.State.fromString( 239 getStringAttr(message.data, 'state')) 240 var error = remoting.ClientSession.ConnectionError.fromString( 241 getStringAttr(message.data, 'error')); 242 this.onConnectionStatusUpdateHandler(state, error); 243 244 } else if (message.method == 'onDesktopSize') { 245 this.desktopWidth = getNumberAttr(message.data, 'width'); 246 this.desktopHeight = getNumberAttr(message.data, 'height'); 247 this.desktopXDpi = getNumberAttr(message.data, 'x_dpi', 96); 248 this.desktopYDpi = getNumberAttr(message.data, 'y_dpi', 96); 249 this.onDesktopSizeUpdateHandler(); 250 251 } else if (message.method == 'onPerfStats') { 252 // Return value is ignored. These calls will throw an error if the value 253 // is not a number. 254 getNumberAttr(message.data, 'videoBandwidth'); 255 getNumberAttr(message.data, 'videoFrameRate'); 256 getNumberAttr(message.data, 'captureLatency'); 257 getNumberAttr(message.data, 'encodeLatency'); 258 getNumberAttr(message.data, 'decodeLatency'); 259 getNumberAttr(message.data, 'renderLatency'); 260 getNumberAttr(message.data, 'roundtripLatency'); 261 this.perfStats_ = 262 /** @type {remoting.ClientSession.PerfStats} */ message.data; 263 264 } else if (message.method == 'injectClipboardItem') { 265 var mimetype = getStringAttr(message.data, 'mimeType'); 266 var item = getStringAttr(message.data, 'item'); 267 if (remoting.clipboard) { 268 remoting.clipboard.fromHost(mimetype, item); 269 } 270 271 } else if (message.method == 'onFirstFrameReceived') { 272 if (remoting.clientSession) { 273 remoting.clientSession.onFirstFrameReceived(); 274 } 275 276 } else if (message.method == 'onConnectionReady') { 277 var ready = getBooleanAttr(message.data, 'ready'); 278 this.onConnectionReadyHandler(ready); 279 280 } else if (message.method == 'fetchPin') { 281 // The pairingSupported value in the dictionary indicates whether both 282 // client and host support pairing. If the client doesn't support pairing, 283 // then the value won't be there at all, so give it a default of false. 284 var pairingSupported = getBooleanAttr(message.data, 'pairingSupported', 285 false) 286 this.fetchPinHandler(pairingSupported); 287 288 } else if (message.method == 'setCapabilities') { 289 /** @type {!Array.<string>} */ 290 var capabilities = tokenize(getStringAttr(message.data, 'capabilities')); 291 this.onSetCapabilitiesHandler(capabilities); 292 293 } else if (message.method == 'fetchThirdPartyToken') { 294 var tokenUrl = getStringAttr(message.data, 'tokenUrl'); 295 var hostPublicKey = getStringAttr(message.data, 'hostPublicKey'); 296 var scope = getStringAttr(message.data, 'scope'); 297 this.fetchThirdPartyTokenHandler(tokenUrl, hostPublicKey, scope); 298 299 } else if (message.method == 'pairingResponse') { 300 var clientId = getStringAttr(message.data, 'clientId'); 301 var sharedSecret = getStringAttr(message.data, 'sharedSecret'); 302 this.onPairingComplete_(clientId, sharedSecret); 303 304 } else if (message.method == 'extensionMessage') { 305 var extMsgType = getStringAttr(message.data, 'type'); 306 var extMsgData = getStringAttr(message.data, 'data'); 307 switch (extMsgType) { 308 case 'gnubby-auth': 309 this.onGnubbyAuthHandler(extMsgData); 310 break; 311 case 'test-echo-reply': 312 console.log('Got echo reply: ' + extMsgData); 313 break; 314 default: 315 if (!this.onExtensionMessage_(extMsgType, extMsgData)) { 316 console.log('Unexpected message received: ' + 317 extMsgType + ': ' + extMsgData); 318 } 319 } 320 321 } else if (message.method == 'mediaSourceReset') { 322 if (!this.mediaSourceRenderer_) { 323 console.error('Unexpected mediaSourceReset.'); 324 return; 325 } 326 this.mediaSourceRenderer_.reset(getStringAttr(message.data, 'format')) 327 328 } else if (message.method == 'mediaSourceData') { 329 if (!(message.data['buffer'] instanceof ArrayBuffer)) { 330 console.error('Invalid mediaSourceData message:', message.data); 331 return; 332 } 333 if (!this.mediaSourceRenderer_) { 334 console.error('Unexpected mediaSourceData.'); 335 return; 336 } 337 // keyframe flag may be absent from the message. 338 var keyframe = !!message.data['keyframe']; 339 this.mediaSourceRenderer_.onIncomingData( 340 (/** @type {ArrayBuffer} */ message.data['buffer']), keyframe); 341 342 } else if (message.method == 'unsetCursorShape') { 343 this.updateMouseCursorImage('', 0, 0); 344 345 } else if (message.method == 'setCursorShape') { 346 var width = getNumberAttr(message.data, 'width'); 347 var height = getNumberAttr(message.data, 'height'); 348 var hotspotX = getNumberAttr(message.data, 'hotspotX'); 349 var hotspotY = getNumberAttr(message.data, 'hotspotY'); 350 var srcArrayBuffer = getObjectAttr(message.data, 'data'); 351 352 var canvas = 353 /** @type {HTMLCanvasElement} */ (document.createElement('canvas')); 354 canvas.width = width; 355 canvas.height = height; 356 357 var context = 358 /** @type {CanvasRenderingContext2D} */ (canvas.getContext('2d')); 359 var imageData = context.getImageData(0, 0, width, height); 360 base.debug.assert(srcArrayBuffer instanceof ArrayBuffer); 361 var src = new Uint8Array(/** @type {ArrayBuffer} */(srcArrayBuffer)); 362 var dest = imageData.data; 363 for (var i = 0; i < /** @type {number} */(dest.length); i += 4) { 364 dest[i] = src[i + 2]; 365 dest[i + 1] = src[i + 1]; 366 dest[i + 2] = src[i]; 367 dest[i + 3] = src[i + 3]; 368 } 369 370 context.putImageData(imageData, 0, 0); 371 this.updateMouseCursorImage(canvas.toDataURL(), hotspotX, hotspotY); 372 } 373 }; 374 375 /** 376 * Deletes the plugin. 377 */ 378 remoting.ClientPlugin.prototype.cleanup = function() { 379 this.plugin.parentNode.removeChild(this.plugin); 380 }; 381 382 /** 383 * @return {HTMLEmbedElement} HTML element that correspods to the plugin. 384 */ 385 remoting.ClientPlugin.prototype.element = function() { 386 return this.plugin; 387 }; 388 389 /** 390 * @param {function(boolean): void} onDone 391 */ 392 remoting.ClientPlugin.prototype.initialize = function(onDone) { 393 if (this.helloReceived_) { 394 onDone(true); 395 } else { 396 this.onInitializedCallback_ = onDone; 397 } 398 }; 399 400 /** 401 * @return {boolean} True if the plugin and web-app versions are compatible. 402 */ 403 remoting.ClientPlugin.prototype.isSupportedVersion = function() { 404 if (!this.helloReceived_) { 405 console.error( 406 "isSupportedVersion() is called before the plugin is initialized."); 407 return false; 408 } 409 return this.API_VERSION_ >= this.pluginApiMinVersion_ && 410 this.pluginApiVersion_ >= this.API_MIN_VERSION_; 411 }; 412 413 /** 414 * @param {remoting.ClientPlugin.Feature} feature The feature to test for. 415 * @return {boolean} True if the plugin supports the named feature. 416 */ 417 remoting.ClientPlugin.prototype.hasFeature = function(feature) { 418 if (!this.helloReceived_) { 419 console.error( 420 "hasFeature() is called before the plugin is initialized."); 421 return false; 422 } 423 return this.pluginApiFeatures_.indexOf(feature) > -1; 424 }; 425 426 /** 427 * @return {boolean} True if the plugin supports the injectKeyEvent API. 428 */ 429 remoting.ClientPlugin.prototype.isInjectKeyEventSupported = function() { 430 return this.pluginApiVersion_ >= 6; 431 }; 432 433 /** 434 * @param {string} iq Incoming IQ stanza. 435 */ 436 remoting.ClientPlugin.prototype.onIncomingIq = function(iq) { 437 if (this.plugin && this.plugin.postMessage) { 438 this.plugin.postMessage(JSON.stringify( 439 { method: 'incomingIq', data: { iq: iq } })); 440 } else { 441 // plugin.onIq may not be set after the plugin has been shut 442 // down. Particularly this happens when we receive response to 443 // session-terminate stanza. 444 console.warn('plugin.onIq is not set so dropping incoming message.'); 445 } 446 }; 447 448 /** 449 * @param {string} hostJid The jid of the host to connect to. 450 * @param {string} hostPublicKey The base64 encoded version of the host's 451 * public key. 452 * @param {string} localJid Local jid. 453 * @param {string} sharedSecret The access code for IT2Me or the PIN 454 * for Me2Me. 455 * @param {string} authenticationMethods Comma-separated list of 456 * authentication methods the client should attempt to use. 457 * @param {string} authenticationTag A host-specific tag to mix into 458 * authentication hashes. 459 * @param {string} clientPairingId For paired Me2Me connections, the 460 * pairing id for this client, as issued by the host. 461 * @param {string} clientPairedSecret For paired Me2Me connections, the 462 * paired secret for this client, as issued by the host. 463 */ 464 remoting.ClientPlugin.prototype.connect = function( 465 hostJid, hostPublicKey, localJid, sharedSecret, 466 authenticationMethods, authenticationTag, 467 clientPairingId, clientPairedSecret) { 468 var keyFilter = ''; 469 if (navigator.platform.indexOf('Mac') == -1) { 470 keyFilter = 'mac'; 471 } else if (navigator.userAgent.match(/\bCrOS\b/)) { 472 keyFilter = 'cros'; 473 } 474 this.plugin.postMessage(JSON.stringify( 475 { method: 'delegateLargeCursors', data: {} })); 476 this.plugin.postMessage(JSON.stringify( 477 { method: 'connect', data: { 478 hostJid: hostJid, 479 hostPublicKey: hostPublicKey, 480 localJid: localJid, 481 sharedSecret: sharedSecret, 482 authenticationMethods: authenticationMethods, 483 authenticationTag: authenticationTag, 484 capabilities: this.capabilities_.join(" "), 485 clientPairingId: clientPairingId, 486 clientPairedSecret: clientPairedSecret, 487 keyFilter: keyFilter 488 } 489 })); 490 }; 491 492 /** 493 * Release all currently pressed keys. 494 */ 495 remoting.ClientPlugin.prototype.releaseAllKeys = function() { 496 this.plugin.postMessage(JSON.stringify( 497 { method: 'releaseAllKeys', data: {} })); 498 }; 499 500 /** 501 * Send a key event to the host. 502 * 503 * @param {number} usbKeycode The USB-style code of the key to inject. 504 * @param {boolean} pressed True to inject a key press, False for a release. 505 */ 506 remoting.ClientPlugin.prototype.injectKeyEvent = 507 function(usbKeycode, pressed) { 508 this.plugin.postMessage(JSON.stringify( 509 { method: 'injectKeyEvent', data: { 510 'usbKeycode': usbKeycode, 511 'pressed': pressed} 512 })); 513 }; 514 515 /** 516 * Remap one USB keycode to another in all subsequent key events. 517 * 518 * @param {number} fromKeycode The USB-style code of the key to remap. 519 * @param {number} toKeycode The USB-style code to remap the key to. 520 */ 521 remoting.ClientPlugin.prototype.remapKey = 522 function(fromKeycode, toKeycode) { 523 this.plugin.postMessage(JSON.stringify( 524 { method: 'remapKey', data: { 525 'fromKeycode': fromKeycode, 526 'toKeycode': toKeycode} 527 })); 528 }; 529 530 /** 531 * Enable/disable redirection of the specified key to the web-app. 532 * 533 * @param {number} keycode The USB-style code of the key. 534 * @param {Boolean} trap True to enable trapping, False to disable. 535 */ 536 remoting.ClientPlugin.prototype.trapKey = function(keycode, trap) { 537 this.plugin.postMessage(JSON.stringify( 538 { method: 'trapKey', data: { 539 'keycode': keycode, 540 'trap': trap} 541 })); 542 }; 543 544 /** 545 * Returns an associative array with a set of stats for this connecton. 546 * 547 * @return {remoting.ClientSession.PerfStats} The connection statistics. 548 */ 549 remoting.ClientPlugin.prototype.getPerfStats = function() { 550 return this.perfStats_; 551 }; 552 553 /** 554 * Sends a clipboard item to the host. 555 * 556 * @param {string} mimeType The MIME type of the clipboard item. 557 * @param {string} item The clipboard item. 558 */ 559 remoting.ClientPlugin.prototype.sendClipboardItem = 560 function(mimeType, item) { 561 if (!this.hasFeature(remoting.ClientPlugin.Feature.SEND_CLIPBOARD_ITEM)) 562 return; 563 this.plugin.postMessage(JSON.stringify( 564 { method: 'sendClipboardItem', 565 data: { mimeType: mimeType, item: item }})); 566 }; 567 568 /** 569 * Notifies the host that the client has the specified size and pixel density. 570 * 571 * @param {number} width The available client width in DIPs. 572 * @param {number} height The available client height in DIPs. 573 * @param {number} device_scale The number of device pixels per DIP. 574 */ 575 remoting.ClientPlugin.prototype.notifyClientResolution = 576 function(width, height, device_scale) { 577 if (this.hasFeature(remoting.ClientPlugin.Feature.NOTIFY_CLIENT_RESOLUTION)) { 578 var dpi = Math.floor(device_scale * 96); 579 this.plugin.postMessage(JSON.stringify( 580 { method: 'notifyClientResolution', 581 data: { width: Math.floor(width * device_scale), 582 height: Math.floor(height * device_scale), 583 x_dpi: dpi, y_dpi: dpi }})); 584 } 585 }; 586 587 /** 588 * Requests that the host pause or resume sending video updates. 589 * 590 * @param {boolean} pause True to suspend video updates, false otherwise. 591 */ 592 remoting.ClientPlugin.prototype.pauseVideo = 593 function(pause) { 594 if (this.hasFeature(remoting.ClientPlugin.Feature.VIDEO_CONTROL)) { 595 this.plugin.postMessage(JSON.stringify( 596 { method: 'videoControl', data: { pause: pause }})); 597 } else if (this.hasFeature(remoting.ClientPlugin.Feature.PAUSE_VIDEO)) { 598 this.plugin.postMessage(JSON.stringify( 599 { method: 'pauseVideo', data: { pause: pause }})); 600 } 601 }; 602 603 /** 604 * Requests that the host pause or resume sending audio updates. 605 * 606 * @param {boolean} pause True to suspend audio updates, false otherwise. 607 */ 608 remoting.ClientPlugin.prototype.pauseAudio = 609 function(pause) { 610 if (!this.hasFeature(remoting.ClientPlugin.Feature.PAUSE_AUDIO)) { 611 return; 612 } 613 this.plugin.postMessage(JSON.stringify( 614 { method: 'pauseAudio', data: { pause: pause }})); 615 }; 616 617 /** 618 * Requests that the host configure the video codec for lossless encode. 619 * 620 * @param {boolean} wantLossless True to request lossless encoding. 621 */ 622 remoting.ClientPlugin.prototype.setLosslessEncode = 623 function(wantLossless) { 624 if (!this.hasFeature(remoting.ClientPlugin.Feature.VIDEO_CONTROL)) { 625 return; 626 } 627 this.plugin.postMessage(JSON.stringify( 628 { method: 'videoControl', data: { losslessEncode: wantLossless }})); 629 }; 630 631 /** 632 * Requests that the host configure the video codec for lossless color. 633 * 634 * @param {boolean} wantLossless True to request lossless color. 635 */ 636 remoting.ClientPlugin.prototype.setLosslessColor = 637 function(wantLossless) { 638 if (!this.hasFeature(remoting.ClientPlugin.Feature.VIDEO_CONTROL)) { 639 return; 640 } 641 this.plugin.postMessage(JSON.stringify( 642 { method: 'videoControl', data: { losslessColor: wantLossless }})); 643 }; 644 645 /** 646 * Called when a PIN is obtained from the user. 647 * 648 * @param {string} pin The PIN. 649 */ 650 remoting.ClientPlugin.prototype.onPinFetched = 651 function(pin) { 652 if (!this.hasFeature(remoting.ClientPlugin.Feature.ASYNC_PIN)) { 653 return; 654 } 655 this.plugin.postMessage(JSON.stringify( 656 { method: 'onPinFetched', data: { pin: pin }})); 657 }; 658 659 /** 660 * Tells the plugin to ask for the PIN asynchronously. 661 */ 662 remoting.ClientPlugin.prototype.useAsyncPinDialog = 663 function() { 664 if (!this.hasFeature(remoting.ClientPlugin.Feature.ASYNC_PIN)) { 665 return; 666 } 667 this.plugin.postMessage(JSON.stringify( 668 { method: 'useAsyncPinDialog', data: {} })); 669 }; 670 671 /** 672 * Sets the third party authentication token and shared secret. 673 * 674 * @param {string} token The token received from the token URL. 675 * @param {string} sharedSecret Shared secret received from the token URL. 676 */ 677 remoting.ClientPlugin.prototype.onThirdPartyTokenFetched = function( 678 token, sharedSecret) { 679 this.plugin.postMessage(JSON.stringify( 680 { method: 'onThirdPartyTokenFetched', 681 data: { token: token, sharedSecret: sharedSecret}})); 682 }; 683 684 /** 685 * Request pairing with the host for PIN-less authentication. 686 * 687 * @param {string} clientName The human-readable name of the client. 688 * @param {function(string, string):void} onDone, Callback to receive the 689 * client id and shared secret when they are available. 690 */ 691 remoting.ClientPlugin.prototype.requestPairing = 692 function(clientName, onDone) { 693 if (!this.hasFeature(remoting.ClientPlugin.Feature.PINLESS_AUTH)) { 694 return; 695 } 696 this.onPairingComplete_ = onDone; 697 this.plugin.postMessage(JSON.stringify( 698 { method: 'requestPairing', data: { clientName: clientName } })); 699 }; 700 701 /** 702 * Send an extension message to the host. 703 * 704 * @param {string} type The message type. 705 * @param {string} message The message payload. 706 */ 707 remoting.ClientPlugin.prototype.sendClientMessage = 708 function(type, message) { 709 if (!this.hasFeature(remoting.ClientPlugin.Feature.EXTENSION_MESSAGE)) { 710 return; 711 } 712 this.plugin.postMessage(JSON.stringify( 713 { method: 'extensionMessage', 714 data: { type: type, data: message } })); 715 716 }; 717 718 /** 719 * Request MediaStream-based rendering. 720 * 721 * @param {remoting.MediaSourceRenderer} mediaSourceRenderer 722 */ 723 remoting.ClientPlugin.prototype.enableMediaSourceRendering = 724 function(mediaSourceRenderer) { 725 if (!this.hasFeature(remoting.ClientPlugin.Feature.MEDIA_SOURCE_RENDERING)) { 726 return; 727 } 728 this.mediaSourceRenderer_ = mediaSourceRenderer; 729 this.plugin.postMessage(JSON.stringify( 730 { method: 'enableMediaSourceRendering', data: {} })); 731 }; 732 733 /** 734 * If we haven't yet received a "hello" message from the plugin, change its 735 * size so that the user can confirm it if click-to-play is enabled, or can 736 * see the "this plugin is disabled" message if it is actually disabled. 737 * @private 738 */ 739 remoting.ClientPlugin.prototype.showPluginForClickToPlay_ = function() { 740 if (!this.helloReceived_) { 741 var width = 200; 742 var height = 200; 743 this.plugin.style.width = width + 'px'; 744 this.plugin.style.height = height + 'px'; 745 // Center the plugin just underneath the "Connnecting..." dialog. 746 var dialog = document.getElementById('client-dialog'); 747 var dialogRect = dialog.getBoundingClientRect(); 748 this.plugin.style.top = (dialogRect.bottom + 16) + 'px'; 749 this.plugin.style.left = (window.innerWidth - width) / 2 + 'px'; 750 this.plugin.style.position = 'fixed'; 751 } 752 }; 753 754 /** 755 * Undo the CSS rules needed to make the plugin clickable for click-to-play. 756 * @private 757 */ 758 remoting.ClientPlugin.prototype.hidePluginForClickToPlay_ = function() { 759 this.plugin.style.width = ''; 760 this.plugin.style.height = ''; 761 this.plugin.style.top = ''; 762 this.plugin.style.left = ''; 763 this.plugin.style.position = ''; 764 }; 765