Home | History | Annotate | Download | only in webapp
      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