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